Appearance
Architecture & extension points
For developers
How the plugin is built, for developers who integrate with it or contribute. The source is public: KommandHub-Org/KmhShipbubbleSW. Namespace Kommandhub\ShipbubbleSW, plugin name KmhShipbubbleSW.
Modules
Feature-first modules under src/. A top-level directory is a boundary; inside it, flat Symfony-style folders (Service, Subscriber, Handler, Struct, Event, Controller).
| Path | Responsibility |
|---|---|
src/KmhShipbubbleSW.php | Bootstrap and lifecycle: runs the installers on install, update and activate; switches the shipping method off on deactivate and uninstall; clean-up on uninstall |
src/Client/ | The only code that talks HTTP to Shipbubble: transport, resources, DTOs |
src/Quote/ | Checkout quotes: receiver address, parcel, rates, cache, failure notices |
src/Shipping/ | Cart processor, courier choice, shipping method installer, sender address |
src/Shipment/ | Booking, re-quote, refresh, cancel; delivery state updates; order snapshot |
src/Webhook/ | Webhook endpoint, signature check, storage, background handler |
src/Flow/ | Flow Builder events, the action and the flow installer |
src/Administration/Controller/ | Admin API routes behind the settings page and the order card |
src/Core/Content/ | DAL entities kmh_shipbubble_shipment, kmh_shipbubble_webhook_event |
src/Setting/Service/Config.php | Typed, sales-channel-aware settings reader |
src/Logging/ConfigurableLogger.php | PSR-3 wrapper gated on the debug settings |
src/ScheduledTask/ | Daily clean-up of old webhook events |
Shipbubble API client
| Layer | Responsibility |
|---|---|
Http/ShipbubbleHttpClient | Base URL, API key per mode, 3-second timeout |
Resource/* | Addresses, rates, couriers, labels, wallet. Failures become ShipbubbleApiException with an ApiErrorReason |
Dto/* | Typed results, read defensively from the JSON |
ShipbubbleClient | The facade services depend on |
Add a call by adding a method to the matching Resource/ class — never by issuing HTTP from a service.
Checkout pipeline
When the customer selects Courier delivery (Shipbubble) (kmh_shipbubble_courier), the cart processor Shipping/ShipbubbleDeliveryProcessor (priority −6000, after the core delivery processor) prices the delivery through Quote/QuoteService:
- Receiver address — validated with Shipbubble once; the address code is stored in
kmh_shipbubble_addressfor good, per sales channel and mode (Shipbubble's codes never expire, and sandbox and live codes are separate). The key is a SHA-256 hash of address, postcode, name, email and phone, ignoring case and extra spaces; nothing readable is stored. A rejected address is cached for one hour. If a rate request is refused because of the receiver address, the code is dropped and the address validated once more. Street, city, country and phone are required. - Parcel —
Quote/PackageBuilderbuilds it from the product line items: weight in kg (default product weight when missing), unit price, quantity, and the first standard Shipbubble box that carries the weight (else the configured default box). - Rates — fetched and cached for the Quote cache time, keyed by sender, receiver, parcel, currency and enabled couriers. Couriers are sorted cheapest first.
- Price — the customer's courier (stored in the sales-channel context — a courier key, never a price) or the cheapest one sets the shipping cost: carrier total plus markup.
The quote is attached to the cart delivery as the extension kmhShipbubbleQuote and rendered by the Twig override storefront/component/shipping/shipping-method.html.twig (block kmh_shipbubble_courier_picker). No JavaScript: the courier radio posts with the core shipping form.
Failures become cart errors (Shipping/Error/ShipbubbleDeliveryError) with the reasons listed on Customer checkout. Reasons only the merchant can fix also create an Administration notification, at most once per sales channel and reason per hour (Quote/FailureNotifier).
On order placement the chosen courier, Shipbubble's request_token, the carrier price and the charged price are written to kmh_shipbubble_shipment — status quoted, or needs_quote when the fallback price was charged.
Booking, refresh and cancel
Shipment/Service/ShipmentService:
- create — books with the checkout
request_tokeninside a transaction holding a row lock (SELECT … FOR UPDATE), so a delivery is never booked twice. When the shipment isneeds_quote, the token is older than 144 hours (ShipmentService::TOKEN_MAX_AGE_HOURS; Shipbubble keeps it for seven days), or Shipbubble rejects it, the delivery is re-quoted (never from the cache) and stored asneeds_quote; the merchant confirms the new carrier price within 15 minutes, then it is booked. A Shipbubble refusal is stored on the shipment (errorCode,errorMessage) and dispatches the failed event; the status stays bookable. - refresh — reads the label back and applies its status.
- cancel — asks Shipbubble; a refusal is stored and returned, the status stays.
After booking, the Shipbubble order id is appended to the order delivery's trackingCodes.
Shipment/Service/DeliveryStateUpdater moves the order delivery with core transitions only: picked_up/in_transit → ship, returned → retour, cancelled → cancel. A transition the current state doesn't allow is logged at info level and skipped. Order and payment states are never touched.
Webhook processing
Webhook/Controller/WebhookControllerreceivesPOST /kmh-shipbubble/webhook/{salesChannelId|default}— see HTTP endpoints.Webhook/Service/WebhookSignatureValidatorchecksx-ship-signature: the HMAC-SHA512 (hex) of the raw body with the scope's sandbox or live API key. Otherwise401and nothing is stored.Webhook/Service/WebhookReceiverstores the body inkmh_shipbubble_webhook_event, keyed by its SHA-256 (a redelivery is acknowledged and not processed again), and dispatchesShipbubbleWebhookMessage. Shipbubble gets its200immediately.Webhook/Message/ShipbubbleWebhookHandler(needs a running worker) takes the new status from the lastpackage_statusentry — the body'sstatusfield is the previous one — and applies it throughShipmentService::applyWebhook(): shipment status, tracking, waybill, courier tracking number, delivery state, Flow event. Events the plugin doesn't use (for exampleshipment.cod.remitted) are stored with a note.
Flow Builder
Events (triggers)
All in Kommandhub\ShipbubbleSW\Flow\Event, extending ShipmentEvent:
| Event name | Class |
|---|---|
kmh_shipbubble.shipment.created | ShipmentCreatedEvent |
kmh_shipbubble.shipment.failed | ShipmentFailedEvent |
kmh_shipbubble.shipment.picked_up | ShipmentPickedUpEvent |
kmh_shipbubble.shipment.in_transit | ShipmentInTransitEvent |
kmh_shipbubble.shipment.delivered | ShipmentDeliveredEvent |
kmh_shipbubble.shipment.returned | ShipmentReturnedEvent |
kmh_shipbubble.shipment.cancelled | ShipmentCancelledEvent |
Each exposes the order (order-aware, so order actions and mail recipients work) and the variables courierName, trackingUrl, status and errorMessage. Subscribe to them like any Symfony event.
Action
action.kmh.shipbubble.create_shipment (Flow/CreateShipmentAction) books every quoted delivery of the order; needs_quote deliveries are left for the merchant.
Installed flows
Flow/FlowInstaller creates the five flows listed on Shipments & flows, active, with fixed ids derived from kmh_shipbubble.flow.<key>, on install, update and activate. A flow is created only when its id is missing, so edited or switched-off flows are left alone (a deleted one is created again). Mail templates are created together with their flow; mail template types kmh_shipbubble_shipment_created|failed|delivered|returned.
Data model
| Table | Holds |
|---|---|
kmh_shipbubble_shipment | One Shipbubble delivery per order delivery: chosen courier, quote, request token, Shipbubble order id, status, tracking, waybill, error |
kmh_shipbubble_webhook_event | Received webhooks, unique per body hash |
order_delivery.kmhShipbubbleShipment exposes the shipment on the order delivery (loaded only when associated). Shipment statuses: quoted, needs_quote, pending, confirmed, picked_up, in_transit, completed, returned, cancelled, failed.
Uninstalling without keep user data drops the shipment, address and webhook tables, deletes the stored sender address codes, the plugin's flows (and any flow using its events or action) and its mail templates. The shipping method is only deactivated, never deleted.
Scheduled task
kmh_shipbubble.webhook_event_cleanup runs daily and deletes stored webhook events older than 90 days. It needs a running worker.
Permissions
kmh_shipbubble_shipment.viewer (kmh_shipbubble_shipment:read, depends on order.viewer) and kmh_shipbubble_shipment.editor (kmh_shipbubble_shipment:update). The create, refresh and cancel routes enforce kmh_shipbubble_shipment:update server-side via _acl.
Logging
Logging/ConfigurableLogger writes to var/log/kmh_shipbubble_<env>.log (rotating, 7 files). error and above are always written; lower levels only with Enable debug logging and the selected levels. Both settings are sales-channel scoped — pass the sales channel in the log context:
php
$this->logger->info('something happened', [
ConfigurableLogger::CONTEXT_SALES_CHANNEL_ID => $salesChannelId,
]);Development & testing
The repository ships a Docker stack running a full Shopware 6.7 with the plugin mounted:
bash
git clone https://github.com/KommandHub-Org/KmhShipbubbleSW.git
cd KmhShipbubbleSW
make up # build and start Shopware
make shell # then: bin/console plugin:install --activate KmhShipbubbleSW| Command | What it does |
|---|---|
make test | PHPUnit (make test FILTER=SomeTest) |
make analyse | PHPStan (level 9) |
make cs / make cs-fix | php-cs-fixer dry-run / apply |
make validate-plugin | shopware-cli Store compliance check |
make zip | Build the distributable ZIP into build/ |
CI runs composer validate, lint, PHPStan, php-cs-fixer and PHPUnit with a 100 % line-coverage gate on every push and pull request. Administration sources live in src/Resources/app/administration/src/; the built bundle in src/Resources/public/ is committed and must be rebuilt with bin/build-administration.sh after changes. Contributions: see CONTRIBUTING.md in the repository (pull requests to main, signed-off commits).