Configuration reference
The bundle exposes the somework_cqrs configuration tree. Every option accepts
a service id or fully-qualified class name. When you pass a class name the
bundle will register it as an autowired, autoconfigured, private service
automatically.
# config/packages/somework_cqrs.yaml
somework_cqrs:
default_bus: messenger.default_bus
buses:
command: messenger.bus.commands
command_async: messenger.bus.commands_async
query: messenger.bus.queries
event: messenger.bus.events
event_async: messenger.bus.events_async
naming:
default: SomeWork\CqrsBundle\Support\ClassNameMessageNamingStrategy
command: app.command_naming_strategy # optional override
query: null # falls back to default
event: null
retry_policies:
command:
default: SomeWork\CqrsBundle\Support\NullRetryPolicy
map:
App\Application\Command\ShipOrder: app.command.retry_policy
App\Domain\Contract\RequiresImmediateRetry: app.command.retry_policy_for_interface
query:
default: app.query_retry_policy
map: {}
event:
default: SomeWork\CqrsBundle\Support\NullRetryPolicy
map:
App\Domain\Event\OrderShipped: app.event.retry_policy
serialization:
default: SomeWork\CqrsBundle\Support\NullMessageSerializer
command:
default: null
map:
App\Application\Command\ShipOrder: app.command.serializer
query:
default: app.query_serializer
map: {}
event:
default: SomeWork\CqrsBundle\Support\NullMessageSerializer
map:
App\Domain\Event\OrderShipped: app.event.serializer
metadata:
default: SomeWork\CqrsBundle\Support\RandomCorrelationMetadataProvider
command:
default: null
map:
App\Application\Command\ShipOrder: app.command.metadata_provider
query:
default: null
map: {}
event:
default: null
map:
App\Domain\Event\OrderShipped: app.event.metadata_provider
dispatch_modes:
command:
default: sync
map:
App\Application\Command\ShipOrder: async
event:
default: sync
map:
App\Domain\Event\OrderShipped: async
transports:
command:
stamp: transport_names
default: []
map:
App\Application\Command\ShipOrder: ['sync_commands']
command_async:
stamp: transport_names
default: ['async_commands']
map:
App\Application\Command\ShipOrder: ['high_priority_async_commands']
query:
stamp: transport_names
default: []
map: {}
event:
stamp: transport_names
default: []
map:
App\Domain\Event\OrderShipped: ['sync_events']
event_async:
stamp: transport_names
default: ['async_events']
map:
App\Domain\Event\OrderShipped:
- 'async_events'
- 'audit_log'
async:
dispatch_after_current_bus:
command:
default: true
map:
App\Application\Command\ShipOrder: false
event:
default: true
map: {}
- default_bus – fallback Messenger bus id. Used whenever a type-specific bus is omitted.
- buses – service ids for the synchronous and asynchronous Messenger buses
backing each CQRS facade. Event buses automatically receive middleware that
ignores
NoHandlerForMessageExceptionfor messages implementingSomeWork\CqrsBundle\Contract\Event, so you can dispatch fire-and-forget events without registering listeners upfront. - naming – strategies implementing
SomeWork\CqrsBundle\Contract\MessageNamingStrategy. They control the human readable message names exposed in CLI tooling and diagnostics. - retry_policies – services implementing
SomeWork\CqrsBundle\Contract\RetryPolicy. Each section defines adefaultservice applied to the entire message type and an optionalmapof message-specific overrides. Keys insidemapmay reference a concrete message class, a parent class, or an interface implemented by the message. The buses merge the returned stamps into each dispatch call so you can tailor retry behaviour per message or shared contracts. - serialization – services implementing
SomeWork\CqrsBundle\Contract\MessageSerializer. Each section mirrors the retry policy structure with a globaldefault, per-typedefault, and a message-specificmap. The buses resolve serializers in that order and append the returnedSerializerStampto the dispatch call when provided. - metadata – services implementing
SomeWork\CqrsBundle\Contract\MessageMetadataProvider. The configuration mirrors the serializer structure with a globaldefault, per-typedefault, and per-messagemap. Providers returnMessageMetadataStampinstances that the bundle appends to dispatched messages. The default provider generates random correlation identifiers, but you can replace it with deterministic implementations for specific messages when required. - dispatch_modes – controls whether commands and events are dispatched
synchronously or asynchronously when callers omit the
DispatchModeargument. Each section defines adefaultmode (syncorasync) plus amapof message-specific overrides. Keys insidemapmay reference a concrete message class, a parent class, or an interface implemented by the message. When a message resolves toasyncthe bundle routes it through the configured asynchronous Messenger bus automatically. If a caller explicitly passes aDispatchMode, that choice always wins. TheCommandBusandEventBusalso exposedispatchSync()anddispatchAsync()helpers that forward todispatch()with the corresponding mode for convenience.CommandBus::dispatchSync()returns the handler result, mirroring the behaviour of theQueryBus. When any command or event resolves toasyncyou must configure the matching Messenger bus viabuses.command_asyncorbuses.event_async. The bundle validates this at container-compilation time and throws anInvalidConfigurationExceptionwhen an async default or override exists without the corresponding async bus id. - transports – lists Messenger transport names that the bundle adds through
TransportNamesStampwhen dispatching messages. Each bus accepts an optionalstampoption that switches to Messenger'sSendMessageToTransportsStamp(available starting in Symfony Messenger 6.3). Selectingsend_messageon an older release triggers a descriptive exception so you can upgrade the dependency. Defaults and overrides are evaluated per bus, so you can send all async commands throughasync_commands, mirror specific events intoaudit_log, or leave sync buses unconfigured. Messenger still applies yourframework.messenger.routingdefinitions after the stamp is attached, and existing routes remain intact when callers provide their ownTransportNamesStamporSendMessageToTransportsStampfor advanced delivery logic. The bundle guards access to Messenger's optional stamp classes, so projects running without them avoid unnecessary autoload attempts. - async.dispatch_after_current_bus – toggles whether the bundle appends
Messenger's
DispatchAfterCurrentBusStampwhen a command or event resolves to the asynchronous bus. Leave thedefaultvalues set totrueto preserve the existing behaviour and enqueue follow-up messages after the current message finishes processing. Use themapto disable the stamp for specific messages that should be sent immediately, even while the current bus is still handling handlers. Additional stamp logic can be plugged in by implementingSomeWork\CqrsBundle\Support\StampDecider, tagging it assomework_cqrs.dispatch_stamp_decider, and letting the bundle run it when commands, queries, or events are dispatched. Queries now honour the same retry, serializer, metadata, and custom stamp hooks as the other CQRS facades.
Message type matching
Resolvers that accept message-specific overrides (retry_policies.map,
serialization.*.map, dispatch_modes.map,
async.dispatch_after_current_bus.*.map, and custom stamp deciders that rely on
MessageTypeLocator) all evaluate the configured keys using the same strategy:
- Exact class matches take priority. When the map contains the concrete message class name the corresponding service is returned immediately.
- Parent classes are checked from the direct parent up to the root of the hierarchy. The first configured class in that chain wins.
- Interfaces (and their parents) are considered last. Interfaces implemented directly by the message are evaluated first, followed by their parents. The order is stable so the most specific interface match is chosen.
This behaviour lets you provide sensible fallbacks without listing every message
explicitly. For example, you can assign a serializer to a shared interface while
overriding a handful of concrete implementations. When no override is found the
resolvers fall back to their type-specific default and finally to the global
default service where applicable.
All options are optional. When you omit a setting the bundle falls back to a safe default implementation that leaves Messenger behaviour unchanged.
When you enable asynchronous defaults you must ensure Messenger workers listen
for the resulting messages. Configure routing in messenger.yaml so that any
message marked async – either via the dispatch_modes defaults or a
per-message override – is delivered to the transport consumed by your workers.
This keeps the CQRS facades consistent with the Messenger routing you already
use for explicit async dispatch calls.
Additionally, define the Messenger bus ids that back asynchronous dispatch
(for example messenger.bus.commands_async or messenger.bus.events_async).
The extension fails fast during container compilation if dispatch_modes
resolve to async while the relevant async bus id remains null, ensuring you
register the transport before deploying.
Handler registry service
The bundle stores compiled handler metadata in the
SomeWork\CqrsBundle\Registry\HandlerRegistry service. You can rely on it to
power diagnostics, smoke tests, or documentation pages. The registry exposes:
all()– returns every handler as a list ofHandlerDescriptorvalue objects.byType('command'|'query'|'event')– limits the descriptors to one message type.getDisplayName(HandlerDescriptor)– resolves a human-friendly name using the configured naming strategies.
Console reference
Five console commands ship with the bundle:
somework:cqrs:list– Prints the handler catalogue in a table. Accepts the--type=<command|query|event>option multiple times. Add--detailsto the command to include the resolved dispatch configuration for every handler. The command is safe to run in production and reflects the container compiled for the current environment.somework:cqrs:generate <type> <class>– Scaffolds a message and handler pair for the chosen type. Optional flags:--handler=to customise the handler class name.--dir=to override the base directory (defaults to<project>/src).--forceto overwrite existing files instead of aborting.somework:cqrs:debug-transports– Audits the Messenger transports that CQRS messages map to, showing both defaults per bus and explicit per-message overrides. Run this command whenever you need to verify routing before shipping configuration changes.somework:cqrs:health– Verifies CQRS infrastructure health: handler resolvability and transport validity. Returns exit code 0 (healthy), 1 (warning), or 2 (critical) — suitable for K8s exec probes and CI pipelines.somework:cqrs:outbox:relay– Reads unpublished messages from the outbox table and dispatches them to transports in stored order. Accepts--limit=N(default 100). Requiresoutbox.enabled: trueanddoctrine/dbal.
All commands are registered automatically when the bundle is enabled.
Inspecting handler configuration
Run the following command to inspect the effective configuration that the bundle applies to each message:
The detailed view adds these columns to every row:
- Dispatch Mode – The default
DispatchModeresolved for the message when callers do not pass an explicit mode. - Async Defers – Whether the bundle applies Messenger's
DispatchAfterCurrentBusStampwhen the message is sent to an asynchronous bus. The column is reported asn/afor message types without async support. - Retry Policy – The
RetryPolicyservice chosen after evaluating the global, per-type, and per-message overrides. - Serializer – The
MessageSerializerservice that will contribute aSerializerStampwhen the message is dispatched. - Metadata Provider – The
MessageMetadataProviderservice that supplies aMessageMetadataStampfor the message, typically containing correlation identifiers.
Use this output to verify how custom overrides are applied across your application or to debug unexpected dispatch behaviour in production environments.
Auditing transport routing
Run php bin/console somework:cqrs:debug-transports to review which Messenger
transports each CQRS bus will target by default and to list every per-message
override. This command surfaces the raw transport mapping compiled into the
container, making it the canonical way to audit routing before deploying
changes.