Command Query Responsibility Segregation
CQRS is a design pattern that splits reading and writing operations into separate concerns i.e. you have a data model dedicated to managing changes to your data and a separate model for reading data. This allows your reads to be tailored specifically for the information you need and not be constrained by the requirements of the write side of the application.
Commands
A "command" is a request to make a change to the system; such as: "create a user" or "activate a thing". Commands are dispatched via a CommandBus that does not return any output. A command should be fully encapsulated with all the necessary data need to action that request. This includes any generated ids before hand i.e.: using this system you should not be relying on database auto-increments or sequences (in this case these are surrogate identities that are used to make database modelling easier). Instead you should only expose UUIDs of the main objects and only if necessary expose internal ids or use an aggregate ID generation strategy such as a counter that increments continually as records are added.
When the command is dispatched, the command bus handles it along with any errors that may occur. These will be raised as an exception that the custom JSON Exception subscriber will collect and transform to API error messages. This can be overridden by adding appropriate error handling.
The command bus uses the following middleware:
- validation
- doctrine_transaction
Additional middlewares can be configured in the config/packages/messenger.yaml
file.
Commands may only be handled by one handler; but a handler may raise more commands to be dispatched if deemed appropriate. However: even in this instance it would be better to write an event listener for a domain event and respond to that as domain events are broadcast after all Doctrine operations have been flushed to the data store.
The command handler may make whatever changes are necessary via calling into the domain models. This includes creating new objects, loading existing ones, interacting with the repository or other services.
Typically your commands will correspond to actual actions that the business carries out and should be named as such.
Queries
A query is a request for information from the system. The query might be "Find me X by Id" or "find all products matching these criteria...". A QueryBus then executes the query command and returns a result. The query encapsulates all the data that has been requested and should never include the originating request object. It is safe to use value objects and primitives. Several abstract query commands are included for basic actions (provided by somnambulist/domain
).
Query commands are immutable and should not be changed; the only concession is if using the includes support to load sub-objects where a with()
method is added.
The query command is handled by a QueryHandler that accepts that command as an argument to the magic __invoke
method. How the query is handled is entirely up to the implementor. It could be pure SQL, API calls, DQL, parse some files, return hard coded responses etc etc.
For example a query command may look like:
<?php
use Somnambulist\Components\Domain\Queries\AbstractQuery;
class FindObjectById extends AbstractQuery
{
private $id;
public function __construct($id)
{
$this->id = $id;
}
public function getId()
{
return $this->id;
}
}
This would then be executed by a QueryHandler that would have the following signature:
<?php
class FindObjectByIdQueryHandler
{
public function __invoke(FindObjectById $query)
{
// do some operations to find the thing
return $object;
}
}
Using a QueryBus allows the query handling to be changed at any time by replacing the query handler with another implementation. For example: we start off with a service that gets large and requires splitting up, queries into the part that is split off do not need to change, only the handler needs updating to make API calls instead and can still return the same objects as before. No changes would be needed in the controllers.
The down side to this approach are many small files; however each of the files is completely testable in isolation.