CQRS stands for Command Query Responsibility Segregation. In brief, the CQRS pattern suggests to separate “write” and “read” models, which it refers to as Commands and Queries. In a web application like a CMS, we perform either “read” operations which return information to the user or “write” operations which modify the data managed by the application.
During Back Office migration to Symfony, PrestaShop needs a way to access and alter data on the new Symfony pages without multiplying the sources of truth, and without accessing
Commands and Queries allow us to isolate the controllers from the data source, which can be later replaced by something else while leaving behind a nice API.
This implementation proposes a “top-down” design, which inverses the classic data-driven design. It starts on a page and the actions performed in it, and then trickles down layer by layer, finishing on the data layer.
In legacy architecture, controller is calling
ObjectModel directly, without providing clear API or separation between read and write model, thus highly coupling data layer with controller. See legacy architecture’s schema below.
Fortunately, by implementing CQRS it allows PrestaShop to quickly build new API, but still use legacy
ObjectModel by implementing
Adapter handlers. This approach enables us to drop
ObjectModel and replace it with something else later without breaking new API (Commands & Queries). See new architecture’s schema below:
Core\Domainnamespace describes business objects, actions and messages. It DOES NOT contain behavior (at least for now).
Commanddescribes a single action. It DOES NOT perform it.
Commandreceives only primitive types on input (int, float, string, bool and array).
Commandthere MUST be at least one
CommandHandlerwhose role is to execute that
CommandHandleracts as a port to a Core, therefore it SHOULD NOT handle the domain logic itself, but orchestrate the necessary services instead.
CommandHandlerMUST be placed in the
Adapternamespace as long as it has legacy dependencies.
CommandHandlerSHOULD NOT return anything on success, and SHOULD throw a typed
Exceptionon failure. The “no return on success” rule can be broken only when creating entities.
CommandHandlerMUST implement an interface containing a single public method like this:
<?php public function handle(NameOfTheCommand $command);
Querydescribes a single data query. It DOES NOT perform it.
Queryreceives only primitive types on input (int, float, string, bool and array).
Querythere MUST be at least one
QueryHandlerwhose role is to execute that
Queryand return the resulting data set.
QueryHandlerSHOULD return a typed object, and SHOULD throw a typed
QueryHandlerSHOULD use the existing
ObjectModelfor reads as long as it’s reasonable to do so (in particular for CUD operations in BO migration).
QueryHandlerMUST be placed in the
Adapternamespace as long as they have legacy dependencies.
QueryHandlerSHOULD return data objects that make sense to the domain, and SHOULD NOT leak internal objects.
QueryHandlerMUST implement an interface containing a single public method and a typed return like this:
<?php /** * @param NameOfTheQuery $query * * @return TypeOfReturn */ public function handle(NameOfTheQuery $query): TypeOfReturn;
Command bus is a pattern used to map
QueryHandlers. PrestaShop defines it’s own
CommandBusInterface and implements it using Tactician command bus library.
PrestaShop uses 2 commands buses:
CommandBus- for dispatching
QueryBus- for dispatching
To help you understand which command/queries are used on a page and how you can interact with them a profiler has been added in the Symfony debug toolbar.
It shows you a quick resume of the CQRS commands/queries used on the page, you can then have more details in the Symfony profiler page: