Appearance
Architecture & queue
For developers
Namespace Kommandhub\SmsSW\ → src/. Plugin class Kommandhub\SmsSW\KommandhubSmsSW.
Modules
| Path | Responsibility |
|---|---|
Flow/Action/SendSmsAction | Flow Builder action action.kommandhub.send.sms. Resolves the recipient, renders the template, dispatches a queue message. Never throws. |
MessageQueue/ | SendSmsMessage (async) and SendSmsHandler. |
Notification/Gateway | NotificationGatewayInterface, RoutingNotificationGateway (ordered fail-over). |
Notification/Provider | NotificationProviderInterface, AbstractHttpNotificationProvider, one directory per vendor, NotificationProviderRegistry, NotificationProviderSelector. |
Notification/Service | RecipientPhoneResolver, TestMessageService, SampleVariableFactory. |
Core/Content/SmsTemplate | Entity kommandhub_sms_template + translation. |
Webhook | POST /notifications/webhook, signature validator, event factory, delivery-report subscriber. |
Storefront | DialCodeProvider (static ISO → dial-code table) and a Twig extension used by the phone-field overrides. |
Administration/Controller | Test message and provider endpoints. |
Send pipeline
text
Flow event
└─ SendSmsAction::handleFlow() (in the request / flow executor)
├─ templateId from the action config
├─ RecipientPhoneResolver: order billing phone → customer default billing phone
│ normalise; null → log + return (flow continues)
├─ load template (flow context language); inactive / empty → log + return
├─ StringTemplateRenderer with the flow data, htmlEscape = false
└─ dispatch SendSmsMessage(recipient, body, dedupeKey, salesChannelId)
async worker
└─ SendSmsHandler
├─ claim dedupeKey in cache.app (24 h) — already claimed → skip
├─ RoutingNotificationGateway::send()
│ NotificationProviderSelector: default → supports(destination) → rest
│ TransientProviderException → next provider; all transient → rethrow
│ PermanentProviderException → stop
├─ transient → release the claim, rethrow → Messenger retry
└─ permanent → log error, swallow (no retry)Messages carry only scalars; rendering happens at dispatch time so an unusable number is skipped inside the flow instead of failing in a worker.
Dedupe key: sms:<templateId>:<recipient>:<orderId|customerId|flowName>. Stored in the cache.app pool; a cache clear inside the retry window can let a duplicate through.
Data model
| Table | Fields |
|---|---|
kommandhub_sms_template | id, mail_template_type_id (FK to core mail_template_type), active, sender_id, timestamps |
kommandhub_sms_template_translation | name, content per language |
Repository service: kommandhub_sms_template.repository. Created by Migration1784497400CreateSmsTemplateTables. No templates are seeded.
sender_id is stored and shown in the Administration but not passed down the send pipeline in 0.9.0-beta.1.
Adding a provider
- Create
src/Notification/Provider/<Vendor>/<Vendor>Provider.phpextendingAbstractHttpNotificationProvider. - Implement
getName(),getLabel(),isConfigured(),send(),verifyCredentials(); overridegetCountryCodes()to claim dial codes. - Add a config card in
config.xmlwith fields named<name><Setting>(ProviderSettings::key()is the one rule), and add the name to thedefaultSmsProvideroptions.
The _instanceof rule in services.yml tags the provider; the registry and the Administration pick it up with no other change.
Throw exactly two exception types from send():
| Exception | Use for | Effect |
|---|---|---|
TransientProviderException | timeout, 429, 5xx, unreadable body | next provider, then queue retry |
PermanentProviderException | 4xx, refusal inside a 2xx | stop, log, no retry |
Using the gateway from another plugin
Other plugins can send through the same routing by injecting Kommandhub\SmsSW\Notification\Gateway\NotificationGatewayInterface:
php
$messageId = $gateway->send('2348030000000', 'Your order is ready.', $salesChannelId);Click and Pick does this as an optional dependency (@? service reference), so it works whether or not the SMS plugin is installed.
Webhooks
POST /notifications/webhook — storefront scope, CSRF and auth disabled, HMAC-verified:
- Header
x-termii-signature=hex(HMAC-SHA512(raw body, webhookSecret)), compared withhash_equals. Missing header or secret →403. - Event type from the payload's
type:outbound→DeliveryReportEvent,inbound→InboundEvent,dnd→DndReportEvent. Unknown types →200 {"status":"ignored"}. DeliveryReportSubscriberlogs message ID and status; nothing is persisted.
Only Termii's header and payload shape are implemented.
Storefront dial codes
Resources/views/storefront/component/address/field/address-phone-number-field.html.twig replaces the phone input with a dial-code <select> plus a national number input; cms-element-form-input.html.twig does the same for CMS forms. The default country comes from the sales channel's country. The table in DialCodeProvider is static; codes are not unique (+1, +7), so the ISO code is the option's identity.
Development setup & testing
bash
git clone https://github.com/KommandHub/KommandhubSmsSW.git
cd KommandhubSmsSW
make up # Shopware + this plugin in container `kommandhub-sms-plugin`
make shell # then: bin/console plugin:install --activate KommandhubSmsSW
make cs-fix && make analyse && make test # the gate to pass before every commit
make test FILTER=SomeTest
make test-coverage
make validate-plugin # Shopware Store compliance (shopware-cli, inside the container)
make zip # release ZIP into build/tests/Unitmirrorssrc/and needs no kernel; tests that boot Shopware carry#[Group('kernel')]and are excluded in CI.- CI (GitHub Actions,
.github/workflows/php.yml) runs composer validate, PHP lint, PHPStan level 9, php-cs-fixer and PHPUnit, and enforces 100 % line coverage. - Provider sends are tested against real provider sandboxes/test senders. The segment counter has a JS spec (
sms-segments.spec.js). make downdeletes the stack's database volume; the stack publishes no host port by default (addports: ["80:80"]to reach the shop in a browser). Shared tooling: Development environment.