Skip to content

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 NoHandlerForMessageException for messages implementing SomeWork\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 a default service applied to the entire message type and an optional map of message-specific overrides. Keys inside map may 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 global default, per-type default, and a message-specific map. The buses resolve serializers in that order and append the returned SerializerStamp to the dispatch call when provided.
  • metadata – services implementing SomeWork\CqrsBundle\Contract\MessageMetadataProvider. The configuration mirrors the serializer structure with a global default, per-type default, and per-message map. Providers return MessageMetadataStamp instances 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 DispatchMode argument. Each section defines a default mode (sync or async) plus a map of message-specific overrides. Keys inside map may reference a concrete message class, a parent class, or an interface implemented by the message. When a message resolves to async the bundle routes it through the configured asynchronous Messenger bus automatically. If a caller explicitly passes a DispatchMode, that choice always wins. The CommandBus and EventBus also expose dispatchSync() and dispatchAsync() helpers that forward to dispatch() with the corresponding mode for convenience. CommandBus::dispatchSync() returns the handler result, mirroring the behaviour of the QueryBus. When any command or event resolves to async you must configure the matching Messenger bus via buses.command_async or buses.event_async. The bundle validates this at container-compilation time and throws an InvalidConfigurationException when an async default or override exists without the corresponding async bus id.
  • transports – lists Messenger transport names that the bundle adds through TransportNamesStamp when dispatching messages. Each bus accepts an optional stamp option that switches to Messenger's SendMessageToTransportsStamp (available starting in Symfony Messenger 6.3). Selecting send_message on 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 through async_commands, mirror specific events into audit_log, or leave sync buses unconfigured. Messenger still applies your framework.messenger.routing definitions after the stamp is attached, and existing routes remain intact when callers provide their own TransportNamesStamp or SendMessageToTransportsStamp for 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 DispatchAfterCurrentBusStamp when a command or event resolves to the asynchronous bus. Leave the default values set to true to preserve the existing behaviour and enqueue follow-up messages after the current message finishes processing. Use the map to 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 implementing SomeWork\CqrsBundle\Support\StampDecider, tagging it as somework_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:

  1. Exact class matches take priority. When the map contains the concrete message class name the corresponding service is returned immediately.
  2. Parent classes are checked from the direct parent up to the root of the hierarchy. The first configured class in that chain wins.
  3. 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 of HandlerDescriptor value 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 --details to 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).
  • --force to 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). Requires outbox.enabled: true and doctrine/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:

$ php bin/console somework:cqrs:list --details

The detailed view adds these columns to every row:

  • Dispatch Mode – The default DispatchMode resolved for the message when callers do not pass an explicit mode.
  • Async Defers – Whether the bundle applies Messenger's DispatchAfterCurrentBusStamp when the message is sent to an asynchronous bus. The column is reported as n/a for message types without async support.
  • Retry Policy – The RetryPolicy service chosen after evaluating the global, per-type, and per-message overrides.
  • Serializer – The MessageSerializer service that will contribute a SerializerStamp when the message is dispatched.
  • Metadata Provider – The MessageMetadataProvider service that supplies a MessageMetadataStamp for 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.