Appearance
Architecture & background processing
For developers
Namespace Kommandhub\OrderExport\ → src/. Plugin class KmhOrderExportSW.
Two layers, one package
| Layer | Contents | Rule |
|---|---|---|
Domain\ | Orchestrator, mapping engine and validator, transformers, payload serialisers and hasher, contracts (transport, authenticator, acknowledgement strategy, stores, clock, lock, circuit breaker), value objects | Framework-agnostic. Imports nothing from Shopware. Enforced by Deptrac (deptrac.yaml). 100 % line coverage gate. |
Shopware\ | DAL definitions, adapters for the Domain ports, admin controllers, Flow action, Messenger message/handler, scheduled task, webhook controller | Thin; kernel-tested. |
The orchestrator receives an opaque Domain\Support\ExportContext so it stays Shopware-free.
Export pipeline
text
ExportOrderFlowAction / admin POST …/export / ExportOrderHandler (async)
└─ ExportOrchestrator::export(ExportRequest)
├─ resolve target (active only) + published mapping version
├─ acquire lock (order × target, 300 s TTL) held → ExportLockException (transient)
├─ OrderProjectionLoader
│ OrderExportCriteriaEvent → one DAL load → OrderProjector (full graph)
│ → OrderExportProjectionEvent
├─ MappingEngine::render() field errors → PayloadRenderException (permanent)
├─ ContentHasher equal to last settled hash → deduplicated
├─ new revision + correlation id on re-export
├─ embed correlation id at correlationPath (after hashing)
├─ PayloadSerializer (json | xml)
├─ CircuitBreaker::guard() open → CircuitOpenException (transient)
├─ Authenticator stamps credentials → OrderExportRequestEvent → Transport::send()
├─ AcknowledgementStrategy (none | synchronous | asynchronous) decides the outcome
└─ settle: state transition + attempt row (payload hash, mapping version, correlation id, HTTP status)State machine
kmh_order_export.state: open, exported, awaiting_acknowledgement, acknowledged, failed. Shopware fires its standard events on every transition (state_machine.kmh_order_export.state.state_changed, state_enter.kmh_order_export.state.<state>), so there are no bespoke lifecycle events.
Data model
| Table | Holds |
|---|---|
kmh_order_export_credential | name, auth type, public meta (header name, username, token URL, client ID, scope), encrypted secret |
kmh_order_export_mapping / …_mapping_version | mapping and its immutable published versions |
kmh_order_export_config (+ …_sales_channel) | export targets |
kmh_order_export | one row per order × target: state, current revision hash, correlation id, acknowledged at |
kmh_order_export_attempt | one row per attempt |
Credential secrets are encrypted with libsodium secretbox; the key is sodium_crypto_generichash(ORDER_EXPORT_ENCRYPTION_KEY ?: APP_SECRET). CredentialLoadedSubscriber strips the ciphertext from every DAL read, so it never leaves through the Admin API; CredentialFactory reads it directly from the database for internal use.
Execution modes
ExportDispatcher reads the target's executionMode:
sync— orchestrator runs inline. A transient failure is returned as a failed outcome, not thrown.async— dispatchesExportOrderMessageto theasynctransport. The admin Export button always runs synchronously.
Retry classification (async)
Retry is Shopware's async Messenger transport policy (default 3 retries, exponential back-off). The plugin only classifies:
| Exception | Class | Messenger |
|---|---|---|
TransportException on connection error or 408/429/5xx | transient | retried |
CircuitOpenException | transient | retried |
ExportLockException | transient | retried |
PermanentFailure marker: ExportConfigNotFound, OrderNotFound, PayloadRenderException, AuthenticationException | permanent | UnrecoverableMessageHandlingException → failed transport at once |
OAuth2: a token endpoint answering 5xx/429 is transient; one answering without a token (bad credentials) is permanent.
Circuit breaker
CacheCircuitBreaker keeps {failures, openUntil} per target URL (xxh128 key) in cache.app. After threshold consecutive failures the circuit opens for cooldown seconds, then allows a half-open trial. A reached endpoint (2xx or business 4xx) resets it. Use a shared cache (Redis) when running several workers or servers.
yaml
# services.yml parameters
kmh_order_export.circuit_breaker.failure_threshold: 5
kmh_order_export.circuit_breaker.cooldown_seconds: 60Scheduled task
kmh_order_export.ack_timeout, every 300 s: loads exports in awaiting_acknowledgement with their target and fails those older than the target's ackConfig.ackTimeoutSeconds (default 3600).
Environment variables
| Variable | Default | Purpose |
|---|---|---|
ORDER_EXPORT_ENCRYPTION_KEY | falls back to APP_SECRET | Key material for credential encryption. Must be stable and identical on all servers. |
Development setup & testing
bash
git clone https://github.com/KommandHub/KmhOrderExportSW.git
cd KmhOrderExportSW
make up # Shopware + this plugin in container `kommandhub-orderexport-plugin`
make shell # then: bin/console plugin:install --activate KmhOrderExportSW
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. make testruns everything including kernel tests without coverage (pcov breaks kernel boot);make test-coverageruns--exclude-group kernel, which is the 100 % Domain gate. Kernel tests need the plugin installed once in the test database:bin/console plugin:install --activate KmhOrderExportSW -e testwith the testDATABASE_URL. Deptrac (deptrac.yaml) enforces thatDomain\never imports Shopware.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.