kommandhub / sms-sw
Transactional SMS for Shopware 6, sent from the Flow Builder alongside your emails, with multi-provider routing for Termii, Sendexa, Africa's Talking and Twilio.
Package info
github.com/KommandHub/KommandhubSmsSW
Type:shopware-platform-plugin
pkg:composer/kommandhub/sms-sw
Requires
- php: >=8.2
- shopware/core: ~6.6.0 || ~6.7.0
- shopware/storefront: ~6.6.0 || ~6.7.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.94
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5
- rregeer/phpunit-coverage-check: ^0.3.1
- shopware/dev-tools: ^1.5
This package is auto-updated.
Last update: 2026-07-24 14:24:42 UTC
README
SMS for Shopware 6
Transactional SMS for Shopware 6, sent alongside core email, with multi-provider direct-carrier routing for African networks.
Table of contents
- Requirements
- Installation
- Configuration
- Architecture
- Features
- Local development
- Makefile commands
- Testing
- Code quality
- CI/CD
- Release process
- Logging and debugging
- Security
- Contributing
- License
Requirements
| Shopware | `~6.6.0 |
| PHP | 8.2 or newer |
| Database | MySQL 8.0+ / MariaDB 10.11+ |
Installation
Via Composer (recommended)
composer require kommandhub/sms-sw bin/console plugin:refresh bin/console plugin:install --activate KommandhubSmsSW bin/console cache:clear
Manual upload
Download the release zip and install it under Extensions > My extensions > Upload extension, then activate it.
Configuration
Settings > Extensions > SMS for Shopware 6
Every setting is sales-channel scoped: use the sales-channel selector at the top of the configuration page to override a value for one channel only.
Architecture
Feature-first modules under src/, following Shopware's own plugin layout. A
top-level directory is a boundary; inside it, flat Symfony-idiomatic folders
(Service, Subscriber, Handler, Struct, Event, Enum, Controller).
No Application/Domain/Infrastructure nesting — one obvious home per class.
Always present:
| Path | Responsibility |
|---|---|
src/KommandhubSmsSW.php |
plugin bootstrap and lifecycle hooks |
src/Setting/Service/Config.php |
typed, sales-channel-aware settings reader |
src/Logging/ConfigurableLogger.php |
PSR-3 wrapper gated on the debug settings |
src/Exception/ |
plugin-scoped exception base |
src/Resources/config/ |
services.yml, config.xml, packages/ |
tests/Unit, tests/Integration |
mirror src/ |
Features
Administration
Vue/JS sources live in src/Resources/app/administration/src/. Built output in
src/Resources/public/ is generated — never hand-edit it, and rebuild with
make zip (or bin/build-administration.sh in the container) after changing a
source file.
main.js— entry point: registers locales, ACL, modules and services.acl/index.js— the plugin's admin permissions.snippet/*.json— one file per locale, mirroring the same key tree.
Any privilege enforced here must also be enforced server-side via _acl on
the route.
Messaging providers
src/Notification/Provider/ is the only place that knows a vendor exists.
| Layer | Responsibility |
|---|---|
NotificationProviderInterface |
the contract: name, label, supportedChannels, configured?, supports?, send, verify |
AbstractHttpNotificationProvider |
transport call, transient/permanent classification, logging |
<Vendor>/<Vendor>Provider |
one directory per vendor — URLs, auth, payload shape |
NotificationProviderRegistry |
every tagged provider, keyed by name; filters by channel |
NotificationProviderSelector |
ordering strategy: per-channel default first, then destination match |
Gateway/RoutingNotificationGateway |
walks that order, fails over on transient errors |
Everything upstream depends on NotificationGatewayInterface and cannot tell
which vendor carried a message, or over which channel.
Shipping providers:
| Provider | Region | Claims |
|---|---|---|
| Termii | West Africa, direct carrier | 234, 233, 225, 221, 254 |
| Sendexa | Ghana | 233 |
| Africa's Talking | East Africa, direct carrier | 254, 256, 255, 250, 265, 251, 234 |
| Twilio | international fallback | everything |
A provider that claims no country codes is willing to carry anything, which is what a fallback wants. A provider that claims some is preferred for those destinations but still offered elsewhere as a last resort — under-claiming costs a preference, never delivery.
Termii's "channel" is a carrier route, not a delivery medium — hence the setting name
termiiRoute(genericordnd). A vendor's vocabulary stays inside its own directory.
Adding a provider
- Create
src/Notification/Provider/<Vendor>/<Vendor>Provider.phpextendingAbstractHttpNotificationProvider. - Implement
getName(),getLabel(),isConfigured(),send(),verifyCredentials(); overridegetCountryCodes()to claim a region. - Add a config card in
config.xmlwhose field names are<name><Setting>—ProviderSettings::key()is the single rule — and add the name to thedefaultSmsProvideroptions.
No existing PHP class changes. The _instanceof tag in services.yml picks the
provider up, the registry keys it, and the administration lists it.
Errors
Providers throw exactly two things, and the distinction is load-bearing:
| Exception | Meaning | Effect |
|---|---|---|
TransientProviderException |
timeout, 429, 5xx, unreadable body | next provider, then queue retry |
PermanentProviderException |
4xx, refusal inside a 2xx | stop; logged, not retried |
Custom entity
One translatable message-template entity, keyed to a core mail_template_type
so an SMS is always a sibling of the email Shopware already sends:
| Entity | Table | Definition |
|---|---|---|
kommandhub_sms_template |
kommandhub_sms_template + kommandhub_sms_template_translation |
src/Core/Content/SmsTemplate/ |
It mirrors the layout of Shopware's own Core/Content/MailTemplate/: the
merchant-editable name and content live in the translation aggregate.
The definition and the migration are two halves of one thing and must stay in step. Migrations are append-only: never edit a released one.
Access them via the generated repositories:
$repository = $container->get('kommandhub_sms_template.repository');
Message queue
NotificationsMessage is dispatched onto Shopware's async transport and handled by
NotificationsMessageHandler in a worker (bin/console messenger:consume async).
Two rules: messages carry ids, not entities, and handlers are idempotent — delivery is at-least-once.
Storefront
src/Resources/app/storefront/src/— JS plugins, registered inmain.js.src/Resources/views/storefront/— Twig overrides. Namespace every block you add ({% block kommandhub_sms_foo %}) so it cannot collide with another plugin extending the same template.src/Resources/snippet/<locale>/— storefront translations; every locale file must carry the same key tree.
Built output under src/Resources/app/storefront/dist/ is generated.
Webhooks
POST /notifications/webhook — public, unauthenticated, HMAC-verified.
WebhookSignatureValidatorrejects anything inauthentic (403). This is the security boundary; everything downstream trusts the payload.WebhookEventFactorymaps the provider's event string to a typed event. Unknown types are logged and answered 200 so the provider stops retrying.- The event is dispatched; subscribers under
Webhook/Subscriber/do the work.
Register the URL in the provider's dashboard per sales channel domain.
Local development
The plugin is developed inside a Docker stack that runs a full Shopware install
with this directory mounted at custom/static-plugins/KommandhubSmsSW.
git clone https://github.com/Kommandhub/KommandhubSmsSW.git cd KommandhubSmsSW make up # build the image, start Shopware, install dependencies make shell # bash into the container # inside the container bin/console plugin:refresh bin/console plugin:install --activate KommandhubSmsSW
Storefront: http://localhost · Administration: http://localhost/admin
(admin / shopware).
Makefile commands
| Command | What it does |
|---|---|
make up / make down |
start / tear down the stack |
make restart |
down then up |
make shell |
shell into the container |
make test |
PHPUnit; filter with make test FILTER=SomeTest |
make test-coverage |
coverage text report |
make analyse |
PHPStan on src/ |
make cs / make cs-fix |
php-cs-fixer dry-run / apply |
make validate-plugin |
shopware-cli store-compliance check |
make changelog |
render CHANGELOG.md as the Store will |
make zip |
build a distributable zip into build/ |
make cli ARGS="..." |
any other shopware-cli command |
Testing
make test make test FILTER=ConfigTest make test-coverage
tests/Unit/mirrorssrc/. No kernel, no database — fast, and what CI runs.tests/Integration/needs a booted Shopware kernel. Mark those tests#[Group('kernel')]; CI runs--exclude-group kernel.- Add or update a test with every behaviour change.
Code quality
make cs-fix && make analyse && make test
All three must pass before a commit. PHPStan runs at the level pinned in
phpstan.dist.neon; php-cs-fixer enforces PSR-12 plus the rules in
.php-cs-fixer.dist.php.
CI/CD
.github/workflows/php.yml runs on every push to main/develop and on every
pull request: composer validate, PHP lint, PHPStan, php-cs-fixer (dry-run),
PHPUnit with coverage, and a coverage threshold gate.
CI runs without a Shopware kernel, so kernel-dependent tests are excluded
there and the plugin bootstrap is excluded from coverage in phpunit.dist.xml.
Release process
- Land everything on
develop; make sure the local gate passes. - Bump
versionincomposer.json. - Add a
# <version>section at the top ofCHANGELOG.md(and the localised variants). Check the rendering withmake changelog. make validate-plugin— must be clean for a Store submission.make zip— the artefact lands inbuild/.- Merge
developintomainand tag the release.
Logging and debugging
Enable debug logging in the plugin configuration; entries land in
var/log/kommandhub_sms_<env>.log (rotating, 7 files).
error and above are always written regardless of the toggle, so production
keeps a trail of failures. Both the toggle and the level filter are
sales-channel scoped — pass the sales channel id in the log context so it
resolves against the right scope:
$this->logger->info('something happened', [ ConfigurableLogger::CONTEXT_SALES_CHANNEL_ID => $salesChannelId, ]);
Security
Report vulnerabilities privately — see SECURITY.md. Never open a public issue for one, and never paste real credentials into an issue.
Contributing
See CONTRIBUTING.md. Pull requests target develop, commits
are signed off (git commit -s), and the local gate must pass.