Download the PHP package etel/cqrs without Composer
On this page you can find all versions of the php package etel/cqrs. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Package cqrs
Short Description Represents contracts and implementation for the CQRS pattern.
License MIT
Homepage https://github.com/etel-soft/cqrs
Informations about the package cqrs
Etel CQRS Component
Contracts and a Symfony Messenger-based implementation of the CQRS pattern for PHP 8.4+.
The library ships three buses — Command, Query and Event — each following the same layering, and lets a handler's result type be declared once, on the message, so that:
- the bus enforces it at runtime (throws on a mismatch);
- static analysis (PHPStan) infers the concrete, optionally-nullable return type of the dispatch call.
Table of contents
- Concepts
- Requirements
- Installation
- Wiring with Symfony Messenger
- Commands
- Queries
- Events
- Declaring & checking handler results
- Inputs: raw data before validation
- Per-bus options
- Exceptions
- Static analysis (PHPStan)
- Development
Concepts
The package is split into two layers:
| Layer | Namespace | Dependencies |
|---|---|---|
| Contracts | Etel\CQRS\{Command,Query,Event}\ |
None — pure PHP |
| Implementation | Etel\CQRS\{Command,Query,Event}\Implementation\ |
symfony/messenger |
The contracts layer has no external dependencies; symfony/messenger is a suggest only. Depend on the contracts
from your domain and only pull the implementation in at the composition root. This keeps your code testable against the
interfaces and lets you swap the transport without touching handlers.
The three buses differ by intent, following Command/Query Separation:
| Bus | Method | Result | Handlers |
|---|---|---|---|
CommandBus |
command() |
Optional (fire-and-forget by default) | Exactly one |
QueryBus |
query() |
Always returned (synchronous) | Exactly one |
EventBus |
publish() |
None (void) |
Zero or more |
Requirements
- PHP 8.4+
symfony/messenger^7.0 || ^8.0 (only for the*\Implementation\classes)
Installation
To use the Messenger-based implementation, also require Messenger:
Wiring with Symfony Messenger
Each bus is a thin decorator over a Messenger MessageBusInterface. Configure one Messenger bus per CQRS bus and
register the decorators.
Middleware order matters. The options middleware runs first (it emits the
ValidationStamp/DelayStampthat later steps rely on); the input transformer runs aftervalidation, so raw input is validated before it becomes the final message.
Mark handlers so the container can autoconfigure them (the library markers are optional but convenient):
AsCommandHandler, AsQueryHandler and AsEventHandler are plain class markers; wire them to a compiler pass or use
them alongside Messenger's own #[AsMessageHandler].
Commands
A command expresses intent to change state. By default it is fire-and-forget.
When you need the handler's result synchronously (e.g. a freshly created id), declare it with #[CommandResult] —
see Declaring & checking handler results.
Queries
A query reads state and always returns a result synchronously (a query that only enqueues work is a command).
A nullable result (e.g. "not found") is declared with nullable: true:
Events
An event is a domain fact that already happened. It may have zero or more handlers and never returns a result. Events carry no validation layer by design — their data is guaranteed valid at the point of emission.
Declaring & checking handler results
The #[QueryResult] / #[CommandResult] attributes declare a message's result once, on the message class. The
bus reads them to verify the handler's return value at runtime, and a bundled PHPStan extension uses them to infer the
dispatch call's return type — so you never repeat the FQCN at the call site.
Queries
A query always returns; the attribute pins the type:
| Attribute | query() return type |
Runtime check |
|---|---|---|
#[QueryResult(Foo::class)] |
Foo |
Result must be a Foo, otherwise throws |
#[QueryResult(Foo::class, nullable: true)] |
Foo\|null |
Foo or null |
#[QueryResult] |
mixed |
none |
| no attribute | mixed |
none |
Commands
A command is fire-and-forget unless it opts in. Omitting the attribute means "returns nothing" (void / async / always-void handler) — that is the canonical way to say a command has no result:
| Attribute | Semantics | command() return type |
|---|---|---|
| no attribute | Void / fire-and-forget | null |
#[CommandResult(Foo::class)] |
Synchronous result of type Foo |
Foo (or Foo\|null with nullable: true) |
#[CommandResult] |
A result is expected, but not a class (e.g. an id) | mixed |
Runtime guarantees
When a result type is declared, the bus throws if the handler violates it:
- the handler returns the wrong type →
UnexpectedQueryResult/UnexpectedCommandResult; - the handler returns
nullbut the attribute is notnullable→ same exception; - the message reaches no handler (e.g. it was routed async) while a result is expected →
InvalidQueryReturnConfiguration/InvalidCommandReturnConfiguration.
$expectResult: the per-call escape hatch
Both command() and query() still accept an explicit $expectResult argument. The attribute is the recommended,
declarative source of truth; $expectResult exists for cases the attribute cannot cover:
- third-party messages you cannot annotate;
- a one-off override at a single call site;
- typing that works without the PHPStan extension installed.
An explicit class-string in $expectResult takes precedence over the attribute.
Inputs: raw data before validation
When a message must contain valid, strongly-typed data (non-nullable properties, enums/VOs instead of raw
strings/ints), use an intermediate input DTO. CommandInput / QueryInput hold raw data and expose
toCommand() / toQuery(), which either return the final typed message or throw if the data is invalid. The
transformer middleware performs this swap after validation, preserving all stamps.
Events have no input equivalent: event data is always valid at emission.
Per-bus options
Each bus takes its own typed options value object instead of raw Messenger stamps. Buses know nothing about each other's options.
| Option VO | Fields |
|---|---|
CommandBusOptions |
int $delayMs = 0, ?list<string> $validationGroups = null |
QueryBusOptions |
?list<string> $validationGroups = null |
EventBusOptions |
int $delayMs = 0, bool $dispatchAfterCurrentBus = false |
The matching *BusOptionsMiddleware translates these into native Messenger stamps (DelayStamp, ValidationStamp,
DispatchAfterCurrentBusStamp).
Exceptions
You catch interfaces (in the contracts layer); the implementation throws concrete classes that implement them.
| Interface | Thrown when |
|---|---|
InvalidCommandData / InvalidQueryData |
Input passed validation but is still invalid for some reason |
UnexpectedCommandPropertyValue / UnexpectedQueryPropertyValue |
A validated property still holds an unexpected value |
InvalidCommandReturnConfiguration / InvalidQueryReturnConfiguration |
A result is expected but the message reached no synchronous handler |
UnexpectedCommandResult / UnexpectedQueryResult |
The handler returned a value that does not match the declared result type |
Static analysis (PHPStan)
The package bundles a PHPStan extension that infers query() / command() return types from the result attributes:
| Call | Inferred type |
|---|---|
query(new GetUserById(1)) with #[QueryResult(UserView::class)] |
UserView |
query(new FindUser(...)) with #[QueryResult(UserView::class, nullable: true)] |
UserView\|null |
query($q, expectResult: UserView::class) |
UserView |
command(new CreateUser(...)) with #[CommandResult(CreatedUser::class)] |
CreatedUser |
command($c) (no attribute) |
null |
If you use phpstan/extension-installer the extension is registered
automatically. Otherwise include it manually:
PhpStorm note. The extension drives PHPStan; it cannot drive PhpStorm's own type inference. The explicit
$expectResult: Foo::classform is understood by PhpStorm natively; attribute-based inference is a PHPStan feature.
Development
This project targets PHP 8.4.
License
Released under the MIT License.