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 ObjectModel
(e.g. Product
, Category
) directly.
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\Domain
namespace describes business objects, actions and messages. It DOES NOT contain behavior (at least for now).Command
.Command
describes a single action. It DOES NOT perform it.Command
receives only primitive types on input (int, float, string, bool and array).Command
there MUST be at least one CommandHandler
whose role is to execute that Command
.CommandHandler
acts as a port to a Core, therefore it SHOULD NOT handle the domain logic itself, but orchestrate the necessary services instead.CommandHandler
MUST be placed in the Adapter
namespace as long as it has legacy dependencies.CommandHandler
SHOULD NOT return anything on success, and SHOULD throw a typed Exception
on failure. The “no return on success” rule can be broken only when creating entities.CommandHandler
MUST implement an interface containing a single public method like this:<?php
public function handle(NameOfTheCommand $command);
Query
.Query
describes a single data query. It DOES NOT perform it.Query
receives only primitive types on input (int, float, string, bool and array).Query
there MUST be at least one QueryHandler
whose role is to execute that Query
and return the resulting data set.QueryHandler
SHOULD return a typed object, and SHOULD throw a typed Exception
on failure.QueryHandler
SHOULD use the existing ObjectModel
for reads as long as it’s reasonable to do so (in particular for CUD operations in BO migration).QueryHandler
MUST be placed in the Adapter
namespace as long as they have legacy dependencies.QueryHandler
SHOULD return data objects that make sense to the domain, and SHOULD NOT leak internal objects.QueryHandler
MUST 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 Commands
and Queries
to CommandHandlers
and QueryHandlers
. PrestaShop defines it’s own CommandBusInterface
and implements it using Tactician command bus library.
PrestaShop uses 2 commands buses:
CommandBus
- for dispatching Commands
onlyQueryBus
- for dispatching Queries
onlyTo 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: