Configuration reference
The bundle is configured under the somework_cqrs key. Every option is
optional: without any configuration the facades use Messenger's default bus,
dispatch everything synchronously and add only a correlation id to each
message. You can print the tree for your installed version with
bin/console config:dump-reference somework_cqrs.
Defaults at a glance
This is the complete tree with its default values:
# config/packages/somework_cqrs.yaml
somework_cqrs:
default_bus: null
buses:
command: null
command_async: null
query: null
event: null
event_async: null
naming:
default: SomeWork\CqrsBundle\Policy\ClassNameMessageNamingStrategy
command: { default: null }
query: { default: null }
event: { default: null }
retry_policies:
default: SomeWork\CqrsBundle\Policy\NullRetryPolicy
command: { default: null, map: {} }
query: { default: null, map: {} }
event: { default: null, map: {} }
retry_strategy:
transports: {}
jitter: 0.0
max_delay: 0
serialization:
default: SomeWork\CqrsBundle\Policy\NullMessageSerializer
command: { default: null, map: {} }
query: { default: null, map: {} }
event: { default: null, map: {} }
metadata:
default: SomeWork\CqrsBundle\Policy\RandomCorrelationMetadataProvider
command: { default: null, map: {} }
query: { default: null, map: {} }
event: { default: null, map: {} }
dispatch_modes:
command: { default: sync, map: {} }
event: { default: sync, map: {} }
transports:
command: { default: [], map: {} }
command_async: { default: [], map: {} }
query: { default: [], map: {} }
event: { default: [], map: {} }
event_async: { default: [], map: {} }
dispatch_after_current_bus:
command: { default: true, map: {} }
event: { default: true, map: {} }
idempotency:
enabled: true
ttl: 300
causation_id:
enabled: true
buses: []
sequence:
enabled: true
rate_limiting:
enabled: true
default: null
command: { default: null, map: {} }
query: { default: null, map: {} }
event: { default: null, map: {} }
outbox:
enabled: false
table_name: somework_cqrs_outbox
connection: default
serializer: messenger.default_serializer
storage: null
auto_setup: true
relay_on_terminate: false
require_transaction: true
max_attempts: 10
signing:
enabled: true
secret: null
previous_secrets: []
accept_unsigned: false
Rules that apply to every section
Service ids and names. Options that name a service or another configured
item (buses, naming strategies, retry policies, serializers, metadata
providers, transport names, rate limiter names, the outbox table, connection
and serializer) must be non-empty strings. Options whose default is
null also accept null. A blank value fails with:
Invalid configuration for path "somework_cqrs.buses.command": Expected a non-empty string or null, got "".
For naming, retry_policies, serialization and metadata you may use a
fully-qualified class name instead of a service id. When no service with that
id exists and the class is concrete, the bundle registers it as a private,
autowired and autoconfigured service. Any other unknown id makes the container
compilation fail with the option that names it:
The service "app.retry.payment" configured at "somework_cqrs.retry_policies.command.map.App\Command\ChargePayment" does not exist.
One shape for every per-message section. retry_policies, serialization,
metadata and rate_limiting have a global default and, per message type, a
default (null falls back to the global one) and a map. naming has the
defaults only. dispatch_modes, transports and dispatch_after_current_bus
have no global default, because it would also apply to synchronous messages.
Options of earlier versions fail with where they moved, e.g.
"somework_cqrs.async.dispatch_after_current_bus" moved to "somework_cqrs.dispatch_after_current_bus".
Per-message maps. Every map is keyed by a message class or interface.
Keys must name an existing class or interface; a leading backslash is removed.
A typo fails the container compilation instead of silently never matching:
Invalid configuration for path "somework_cqrs.retry_policies.command.map": "App\Command\ShipOrdr" is not an existing class or interface; keys must be message class or interface names.
Compile-time flags. The enabled flags of outbox, idempotency,
causation_id, sequence and rate_limiting decide which services exist, so
they must be literal booleans. An environment variable (%env(...)%) is
rejected:
"somework_cqrs.outbox.enabled" decides which services are registered when the container is compiled, so it must be a boolean and cannot use an environment variable.
Environment variables. Only options read at runtime accept %env(...)%:
retry_strategy.jitter, retry_strategy.max_delay, idempotency.ttl,
outbox.auto_setup, outbox.max_attempts, outbox.signing.secret,
outbox.signing.previous_secrets, outbox.signing.accept_unsigned and the
dispatch_after_current_bus flags. Every other option names services,
buses, transports, dispatch modes or message classes that the container
compilation needs, and rejects an environment variable:
"somework_cqrs.dispatch_modes.command.default" is used when the container is compiled (it names services, buses, transports, dispatch modes or message classes), so it cannot use an environment variable.
Resolution order for per-message maps
All map sections resolve a message the same way. For a dispatched message the
bundle looks for, in this order:
- an entry for the exact message class;
- an entry for a parent class, from the direct parent up;
- an entry for an interface the message implements (including interfaces inherited from parents and parent interfaces);
- the
defaultof the message type section; - the global
defaultof the section, where it has one.
What happens when nothing matches depends on the section:
| Section | Fallback after the map |
|---|---|
retry_policies.<type> |
retry_policies.<type>.default, then retry_policies.default |
serialization.<type> |
serialization.<type>.default, then serialization.default |
metadata.<type> |
metadata.<type>.default, then metadata.default |
rate_limiting.<type> |
rate_limiting.<type>.default, then rate_limiting.default, then no limiter |
transports.<bus> |
transports.<bus>.default; when that is empty, no stamp is added and Messenger routing applies |
dispatch_after_current_bus.<type> |
dispatch_after_current_bus.<type>.default |
Dispatch modes follow a slightly different order because the
#[Outbox] and #[Asynchronous] attributes take part. For a command or event dispatched with
DispatchMode::DEFAULT (that is, dispatch() without a mode):
- an entry for the exact class in
dispatch_modes.<type>.map; - the
#[Outbox]attribute on the message class (SomeWork\CqrsBundle\Attribute\Outbox) selectsoutbox, the#[Asynchronous]attribute (SomeWork\CqrsBundle\Attribute\Asynchronous) selectsasync(a class carrying both fails the compilation); - an entry for a parent class;
- an entry for an interface, the most derived interface first;
dispatch_modes.<type>.default.
An explicit mode always wins: dispatch($message, DispatchMode::ASYNC),
dispatch($message, DispatchMode::OUTBOX), dispatchSync() and
dispatchAsync() skip this resolution. Queries are always
synchronous.
default_bus
| Type | service id or null |
| Default | null, which means messenger.default_bus |
The Messenger bus used for buses.command, buses.query and buses.event
when they are not set. The async buses never fall back to it. It gets the
bundle's middleware (causation id, OpenTelemetry, outbox, …) only while one of
these three falls back to it: with all three set, the default bus is left alone.
buses
| Key | Default | Purpose |
|---|---|---|
command |
null (falls back to default_bus) |
Bus used by CommandBus for synchronous dispatch |
command_async |
null (async commands disabled) |
Bus used by CommandBus for asynchronous dispatch |
query |
null (falls back to default_bus) |
Bus used by QueryBus |
event |
null (falls back to default_bus) |
Bus used by EventBus for synchronous dispatch |
event_async |
null (async events disabled) |
Bus used by EventBus for asynchronous dispatch |
Values are Messenger bus service ids, as declared under
framework.messenger.buses.
framework:
messenger:
default_bus: messenger.bus.commands
buses:
messenger.bus.commands: ~
messenger.bus.commands_async: ~
messenger.bus.queries: ~
messenger.bus.events: ~
messenger.bus.events_async: ~
somework_cqrs:
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
- A handler without an explicit
busis registered on the synchronous bus of its type and, when configured, on the async bus of that type, so a worker consuming async messages finds it. A handler with an explicitbusis registered on that bus only. - The event buses (
event, ordefault_buswheneventis not set, andevent_async) receiveAllowNoHandlerMiddleware: anEventwithout handlers is not an error. Commands and queries without a handler still fail. - Leaving an async bus at
nulldisables async dispatch for that type. An async dispatch then throwsAsyncBusNotConfiguredException. When the configuration itself asks for async delivery (anasyncdispatch mode, or entries undertransports.command_async/transports.event_async), the container compilation fails instead. With the outbox enabled, thetransports.*_asyncentries need no async bus: messages stored in the outbox use them. Anasyncdispatch mode still needs the bus:
Asynchronous dispatch is configured for commands (the default dispatch mode is "async"), but "somework_cqrs.buses.command_async" is null. Define the Messenger bus id used for async commands before the container is compiled.
messenger.bus, or an alias of one); anything else fails the compilation:
"somework_cqrs.buses.event_async" is "event.asyn_bus", which is not a Messenger bus. Known buses: command.bus, event.async_bus, event.bus. Declare it under "framework.messenger.buses".
naming
| Key | Default |
|---|---|
default |
SomeWork\CqrsBundle\Policy\ClassNameMessageNamingStrategy |
command.default, query.default, event.default |
null (use the global default) |
Services implementing SomeWork\CqrsBundle\Contract\MessageNamingStrategy
(getName(string $messageClass): string). They only produce the display names
shown by somework:cqrs:list and HandlerRegistry::getDisplayName(); they do
not affect routing or serialization. The default strategy returns the short
class name.
retry_policies
A global default plus one section per message type (command, query,
event):
| Key | Default | Allowed values |
|---|---|---|
default |
SomeWork\CqrsBundle\Policy\NullRetryPolicy |
service id or class name |
<type>.default |
null (use the global default) |
service id or class name |
<type>.map |
{} |
message class or interface => service id or class name |
Services implement SomeWork\CqrsBundle\Contract\RetryPolicy:
<?php
namespace SomeWork\CqrsBundle\Contract;
use SomeWork\CqrsBundle\Bus\DispatchMode;
use Symfony\Component\Messenger\Stamp\StampInterface;
interface RetryPolicy
{
/**
* @return list<StampInterface>
*/
public function getStamps(object $message, DispatchMode $mode): array;
}
The returned stamps are added to every dispatch of a matching message, except
stamps of a class the caller already passed. When
the policy also implements SomeWork\CqrsBundle\Contract\RetryConfiguration
(getMaxRetries(), getInitialDelay(), getMultiplier()), transports listed
under retry_strategy use these values to retry failed
messages.
The bundle ships two policies:
NullRetryPolicyadds nothing.ExponentialBackoffRetryPolicyadds no stamps and implementsRetryConfiguration(defaults: 3 retries, 1000 ms initial delay, multiplier 2.0). It is registered with these defaults assomework_cqrs.exponential_backoff_retry_policy; define your own service of the class to change the arguments (see Retry strategy bridge).
Resolution: exact class, parent classes, interfaces, the type default, then
the global default.
somework_cqrs:
retry_policies:
command:
map:
App\Application\Command\ProcessPayment: somework_cqrs.exponential_backoff_retry_policy
App\Domain\Contract\RetriesAggressively: app.retry.aggressive
retry_strategy
Replaces the retry strategy of Messenger transports with CqrsRetryStrategy,
which applies the per-message RetryConfiguration described above.
| Key | Default | Allowed values |
|---|---|---|
transports |
{} |
list of Messenger transport names, or transport name => command, query or event |
jitter |
0.0 |
float between 0.0 and 1.0 |
max_delay |
0 |
integer >= 0, in milliseconds; 0 means no cap |
- Keys of
transportsare Messenger transport names, kept exactly as written. A name that is not a transport fails the compilation:
Transport "async_comands" configured under "somework_cqrs.retry_strategy.transports" is not a Messenger transport. Known transports: "async_commands", "failed".
- Each message received from the transport uses the
retry_policiessection of its own type, so commands and events can share a transport. The value is the section used for messages that are neither commands, queries nor events; a plain list (transports: [async]) usescommandfor them. - For a message whose policy implements
RetryConfiguration, the message is retried while its retry count is belowgetMaxRetries(), with a delay ofinitialDelay * multiplier ^ retryCount. The delay is capped atmax_delay, then varied by up tojitterin both directions (and capped again). - Any other message falls back to the transport's own strategy, configured under
framework.messenger.transports.<name>.retry_strategy(Messenger's defaults: 3 retries, 1000 ms delay, multiplier 2).
somework_cqrs:
retry_strategy:
transports:
async_commands: command
async_events: event
jitter: 0.1
max_delay: 60000
serialization
| Key | Default | Allowed values |
|---|---|---|
default |
SomeWork\CqrsBundle\Policy\NullMessageSerializer |
service id or class name |
command.default, query.default, event.default |
null (use serialization.default) |
service id, class name or null |
command.map, query.map, event.map |
{} |
message class or interface => service id or class name |
Services implement SomeWork\CqrsBundle\Contract\MessageSerializer:
<?php
namespace SomeWork\CqrsBundle\Contract;
use SomeWork\CqrsBundle\Bus\DispatchMode;
use Symfony\Component\Messenger\Stamp\SerializerStamp;
interface MessageSerializer
{
public function getStamp(object $message, DispatchMode $mode): ?SerializerStamp;
}
A returned SerializerStamp (Messenger's serializer context) is added to the
dispatch; null adds nothing. The default serializer returns null. A
SerializerStamp passed by the caller wins and the serializer service is not
called.
Resolution: exact class, parent classes, interfaces, type default, global
default.
somework_cqrs:
serialization:
event:
default: app.event_serializer_context
map:
App\Domain\Event\OrderShipped: app.order_shipped_serializer_context
metadata
| Key | Default | Allowed values |
|---|---|---|
default |
SomeWork\CqrsBundle\Policy\RandomCorrelationMetadataProvider |
service id or class name |
command.default, query.default, event.default |
null (use metadata.default) |
service id, class name or null |
command.map, query.map, event.map |
{} |
message class or interface => service id or class name |
Services implement SomeWork\CqrsBundle\Contract\MessageMetadataProvider:
<?php
namespace SomeWork\CqrsBundle\Contract;
use SomeWork\CqrsBundle\Bus\DispatchMode;
use SomeWork\CqrsBundle\Stamp\MessageMetadataStamp;
interface MessageMetadataProvider
{
public function getStamp(object $message, DispatchMode $mode): ?MessageMetadataStamp;
}
A provider returns a new stamp for each call (or null): the stamp carries the
message id. The default provider adds a MessageMetadataStamp with a random
32-character hexadecimal message id, which is also its correlation id. A
MessageMetadataStamp passed by the caller wins (use it to propagate a
correlation id you received). When a message is dispatched from inside a
handler, it inherits the correlation id of the handled message (see
causation_id).
Resolution: exact class, parent classes, interfaces, type default, global
default.
somework_cqrs:
metadata:
command:
map:
App\Application\Command\ImportCatalog: app.import_metadata_provider
dispatch_modes
One section each for command and event (queries are always synchronous):
| Key | Default | Allowed values |
|---|---|---|
default |
sync |
sync, async or outbox |
map |
{} |
message class or interface => sync, async or outbox |
The mode is used when the caller does not choose one (dispatch() with
DispatchMode::DEFAULT). See the resolution order
above, including the #[Outbox] and #[Asynchronous] attributes. Any async
value requires the matching async bus (buses.command_async / buses.event_async),
and any outbox value requires outbox.enabled (see
Through the buses). An invalid value fails the
compilation:
Invalid configuration for path "somework_cqrs.dispatch_modes.command.map.App\Application\Command\GenerateReport": Invalid dispatch mode ""later"". Expected "sync", "async" or "outbox".
somework_cqrs:
dispatch_modes:
command:
default: sync
map:
App\Application\Command\GenerateReport: async
event:
default: async
map:
App\Domain\Event\PasswordChanged: sync
transports
Five sections: command, command_async, query, event and event_async.
The section is chosen by message type and resolved dispatch mode (a command
dispatched asynchronously uses command_async).
| Key | Default | Allowed values |
|---|---|---|
default |
[] |
list of transport names (a single string is accepted) |
map |
{} |
message class or interface => list of transport names (or one string) |
When a transport list resolves for a message, the bundle adds Messenger's
TransportNamesStamp. The stamp replaces framework.messenger.routing for that
message: Messenger sends it to exactly these transports.
- Every transport name must be defined under
framework.messenger.transports; otherwise compilation fails withMessenger transport "..." configured for SomeWork CQRS is not defined. - The
*_asyncsections are used for asynchronous dispatches through the matching async bus and, with the outbox enabled, for messages stored in the outbox, which need no async bus. Without the async bus and the outbox, any entry in them fails the compilation (seebuses). - A
TransportNamesStamppassed by the caller wins. On asynchronous dispatches,#[Asynchronous(transport: '...')]beats parent/interface entries and thedefault, but not an entry for exactly the message class; a bare#[Asynchronous]only adds theasynctransport when nothing is configured and neitherframework.messenger.routingnor#[AsMessage(transport: ...)]routes the message. - Transports on the synchronous sections (
command,query,event) send the message away instead of handling it in-process (unless the transport issync://).CommandBus::dispatchSync()andQueryBus::ask()then throwMessageSentToTransportException.
Resolution: exact class, parent classes, interfaces, then the section
default. When nothing resolves, no stamp is added and Messenger's routing
applies.
somework_cqrs:
transports:
command_async:
default: async_commands
map:
App\Application\Command\ShipOrder: [high_priority]
event_async:
default: [async_events]
map:
App\Domain\Event\OrderShipped: [async_events, audit_log]
dispatch_after_current_bus
Controls Messenger's DispatchAfterCurrentBusStamp on asynchronous dispatches.
With the stamp, a message dispatched while another message is being handled is
only sent after that handler finished successfully, and dropped when it fails.
| Key | Default | Allowed values |
|---|---|---|
command.default, event.default |
true |
boolean |
command.map, event.map |
{} |
message class or interface => boolean |
- Applies to asynchronous dispatches only.
- A
DispatchAfterCurrentBusStamppassed by the caller is kept bydispatch()anddispatchAsync(), except for a message stored in the outbox, which is stored at once (OutboxPrepareMiddlewaredrops the stamp). dispatchSync()andask()remove a stamp passed by the caller, because they need the result immediately.
Resolution: exact class, parent classes, interfaces, then the type default.
idempotency
Bridges the bundle's IdempotencyStamp to Messenger's deduplication.
| Key | Default | Allowed values |
|---|---|---|
enabled |
true |
boolean (no environment variables) |
ttl |
300 |
integer >= 1, lock lifetime in seconds |
When a dispatch carries new IdempotencyStamp('key'), the bundle adds a
DeduplicateStamp whose key is <message class>::<key> and whose TTL is
ttl. The IdempotencyStamp stays on the envelope, and a DeduplicateStamp
passed by the caller is kept.
Deduplication only happens when all of the following hold. Missing pieces do not fail the build; the reason is written to the container compilation log.
- symfony/lock is installed;
- the lock component is enabled (
framework.lock), which registers Messenger's deduplicate middleware, with a lock store shared by all processes that keeps locks until their TTL expires (see Production: idempotency).
When a synchronous dispatch fails, the bundle releases the lock again, so a retry with the same key is not rejected. See Idempotency.
causation_id
| Key | Default | Allowed values |
|---|---|---|
enabled |
true |
boolean (no environment variables) |
buses |
[] |
list of Messenger bus service ids |
While a handler runs, CausationIdMiddleware keeps the MessageMetadataStamp
of the message being handled. Messages dispatched from that handler get its
message id as their causation id (an explicit causation id is kept) and, unless
the caller passed its own stamp, its correlation id. With enabled: false,
every message starts its own flow.
buses limits the middleware to the listed buses; the empty default means all
buses used by the bundle: every configured buses.*, and default_bus when a
facade falls back to it (one of buses.command, buses.query or buses.event is
not set). The
other CQRS buses get a variant that only hides the outer message: messages
dispatched by their handlers start a new flow instead of naming an unrelated
message as their cause. Each entry must be a Messenger bus:
"somework_cqrs.causation_id.buses" contains "messenger.bus.comands", which is not a Messenger bus service id.
enabled: false removes both the middleware and the stamp decider.
sequence
| Key | Default | Allowed values |
|---|---|---|
enabled |
true |
boolean (no environment variables) |
Events implementing SomeWork\CqrsBundle\Contract\SequenceAware
(getAggregateType(), getAggregateId(), getSequenceNumber()) receive an
AggregateSequenceStamp with these three values. A stamp
passed by the caller is kept. See Event ordering.
rate_limiting
| Key | Default | Allowed values |
|---|---|---|
enabled |
true |
boolean (no environment variables) |
default |
null |
rate limiter name applied to every message without a more specific entry |
command.default, query.default, event.default |
null (use the global default) |
rate limiter name |
command.map, query.map, event.map |
{} |
message class or interface => rate limiter name |
Values are limiter names from framework.rate_limiter (the bundle uses the
limiter.<name> service; a name that is not defined fails the compilation).
Rate limiting is inactive while no limiter is configured. Configuring a limiter
requires symfony/rate-limiter:
Rate limiters are configured under "somework_cqrs.rate_limiting" but symfony/rate-limiter is not installed. Run "composer require symfony/rate-limiter" or remove them.
Each dispatch of a matching message consumes one token. The limiter key is the
message class, so a default gives every message class its own bucket, not one
shared bucket. When no token is left, the dispatch throws
RateLimitExceededException before anything is sent. Resolution: exact class,
parent classes, interfaces, the type default, then the global default;
messages without any of them are not limited. See
Rate limiting.
framework:
rate_limiter:
send_notification:
policy: sliding_window
limit: 100
interval: '1 minute'
somework_cqrs:
rate_limiting:
command:
map:
App\Application\Command\SendNotification: send_notification
outbox
| Key | Default | Allowed values |
|---|---|---|
enabled |
false |
boolean (no environment variables) |
storage |
null |
service id or class name of an OutboxStorage; null for the DBAL storage below |
table_name |
somework_cqrs_outbox |
letters, digits and underscores, optionally schema.table (database.table on MySQL); avoid reserved SQL words |
connection |
default |
DBAL connection name; the service doctrine.dbal.<name>_connection (DoctrineBundle) is used |
serializer |
messenger.default_serializer |
Messenger serializer service id; aliased as somework_cqrs.outbox.serializer |
auto_setup |
true |
boolean |
relay_on_terminate |
false |
boolean, for development: runs the relay after each request, console command or worker message that stored messages in the outbox (a plain boolean, not an %env()% value) |
require_transaction |
true |
boolean: OutboxWriter and DispatchMode::OUTBOX refuse to store outside a transaction on the outbox connection (OutboxRequiresTransactionException); checked with storages that implement TransactionalOutbox |
max_attempts |
10 |
integer, at least 1: attempts before the relay gives up on a row (three times as many when its transport fails) |
signing.enabled |
true |
boolean (no environment variables): sign stored rows and verify them before the relay decodes them |
signing.secret |
null |
string; null uses kernel.secret (framework.secret) |
signing.previous_secrets |
[] |
list of strings: secrets whose signatures are still accepted |
signing.accept_unsigned |
false |
boolean: relay rows without a signature (e.g. of an earlier version) |
Enabling the outbox registers the SomeWork\CqrsBundle\Contract\Outbox\OutboxStorage
service plus the somework:cqrs:outbox:* commands. With storage: null that is
DbalOutboxStorage, which requires doctrine/dbal (compilation fails otherwise)
and uses table_name, connection and auto_setup; another storage ignores
them (see Custom storage).
- Use the connection that holds your business data, so storing an outbox row is part of the same transaction.
- With
auto_setup: truethe table is created (or upgraded with the columns added in 0.5.0, but not the index, which is left tosomework:cqrs:outbox:setup) on first use, but never inside an open transaction: that throws aLogicExceptionasking you to runsomework:cqrs:outbox:setup. Disableauto_setupwhen migrations manage the table. With doctrine/orm installed, the table is also added to the schema of the outbox connection, sodoctrine:migrations:diffpicks it up. - The relay decodes stored rows with
serializer.OutboxWriterencodes with it; encode with the same serializer when you callOutboxMessage::fromEnvelope()yourself.
See Transactional outbox.
Console commands
All commands are registered automatically; the somework:cqrs:outbox:* commands
only when outbox.enabled is true. Exit codes follow Symfony's convention:
0 success, 1 failure, 2 invalid input.
| Command | Arguments and options | Exit codes |
|---|---|---|
somework:cqrs:list |
[--type=TYPE ...] [--message=TEXT] [--details] |
0; 2 for an unknown --type |
somework:cqrs:generate |
<type> <name> [--handler=FQCN] [--dir=DIR] [--force] |
0; 1 when a file exists (without --force) or cannot be written; 2 for an invalid type, class name or path |
somework:cqrs:debug-transports |
none | 0 |
somework:cqrs:health |
none | 0 OK, 1 warnings, 2 critical |
somework:cqrs:outbox:setup |
none | 0; 1 for a storage that does not implement OutboxSchema, or when the database fails; 128 + signal when stopped by a signal |
somework:cqrs:outbox:relay |
[--limit=100] (-l) [--watch (-w) [--sleep=1] [--time-limit=SECONDS]] [--wait-for-lock=0] [--no-reset] |
0, also when another relay holds the lock (without --wait-for-lock) or --watch was stopped by a signal or --time-limit; 1 when a row failed, the storage failed, a signal stopped a single run, or the lock could not be acquired or was lost; 2 for invalid options; 3 when another relay kept the lock for --wait-for-lock seconds |
somework:cqrs:outbox:failed |
[--requeue [--transport=NAME] [--sign [--allow-class=CLASS ...]]] [--delete] [<id> ...] [--limit=50] (-l) |
0; 1 for a storage that does not implement FailedOutboxMessages, when the database fails, when a given id was not requeued or deleted, or when --sign refused or the operator did not confirm; 2 for ids without --requeue or --delete, --transport, --sign or --delete without ids, --delete with --requeue, --sign or --transport, --allow-class without --sign, or an invalid limit |
somework:cqrs:outbox:purge |
[--older-than="7 days"] |
0; 2 for an invalid age |
somework:cqrs:list
Prints one table per message type with a row per handler and bus: the message
class, its display name when the naming strategy gives it another
name than the class name, the handler class and the bus. --type accepts
command, query or event and can be repeated; --message keeps the messages
whose class or display name contains the text (case-insensitive). --details
prints one table per handler with its service id and the configuration the
bundle resolves for the message:
- Dispatch Mode: the mode used for
DispatchMode::DEFAULT. - Async Defers: whether async dispatches get
DispatchAfterCurrentBusStamp(n/afor queries). - Sync Transports / Async Transports: transports from the
transportsconfiguration (Nonewhen nothing is configured, so Messenger routing applies;n/afor the async column of queries). - Retry Policy, Serializer, Metadata Provider: the resolved service
classes. A retry policy other than
NullRetryPolicyis marked as not used while no transport is listed underretry_strategy.transports.
The details are resolved on an instance created without calling the
constructor; they show n/a for abstract classes and interfaces.
somework:cqrs:generate
Creates a message and a handler for <type> (command, query or event)
and <name>, the fully-qualified message class. The handler class defaults to
<name>Handler; change it with --handler.
Files follow the PSR-4 mapping in the project's composer.json
(App\Command\ShipOrder becomes src/Command/ShipOrder.php for "App\\": "src/"); a
namespace that no prefix covers is refused unless --dir is given.
--dir is relative to the project directory and replaces the directory mapped
to the namespace prefix. Existing files are only overwritten with --force.
The generated handler uses the attribute and a typed __invoke().
somework:cqrs:debug-transports
Prints the transports configuration: the default transports and the
per-message overrides of each of the five sections. It does not show
framework.messenger.routing or #[Asynchronous] transports; inspect the
routing with bin/console debug:config framework messenger.routing.
somework:cqrs:health
Instantiates every CQRS handler service and every Messenger transport and reports problems in a table. The exit code is the highest severity found. See Production: health checks for details and custom checks.
Outbox commands
somework:cqrs:outbox:setupcreates the outbox table if it does not exist, and adds the columns and indexes a table of an earlier version lacks (the automatic setup only adds the columns).somework:cqrs:outbox:relaysends due rows (transports take turns; new rows first, then retries) and marks them published. A row that fails is retried later (1 minute, doubling up to 1 hour) and makes the command exit with1; aftermax_attemptsattempts (three times as many for transport failures) it is given up. A transport that fails 3 times in a row (10 times, or 3 over 10 seconds, after a successful send) is paused until the next run.somework:cqrs:outbox:failedlists the given-up rows with their last error;--requeuehands all of them, or the given ids, back to the relay, to another transport with--transport. A row stored for a transport that does not exist is given up on its first run.somework:cqrs:outbox:purgedeletes rows published before the given age.
See Production: outbox operations.
Handler registry service
SomeWork\CqrsBundle\Registry\HandlerRegistry exposes the handler map compiled
into the container (it backs somework:cqrs:list and the health check). It is
part of the public API (@api); get it from the container (autowire
HandlerRegistry) rather than constructing it.
all()returns every handler as a list ofHandlerDescriptorobjects (type,messageClass,handlerClass,serviceId,bus). A handler registered on a sync and an async bus appears once per bus.byType(MessageType::Command)(SomeWork\CqrsBundle\Registry\MessageType:Command,Query,Event) limits the list to one message type;HandlerDescriptor::$typeis aMessageTypetoo.getDisplayName(HandlerDescriptor $descriptor)returns the name produced by the configured naming strategy.