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);
In this example, we’ll create a Command to toggle the status of a Product.
We’ll assume you’re creating it inside a module, which is the recommended way to extend PrestaShop.
If you’re not familiar with module creation yet, see how to create a module.
First declare the CommandHandler in the config/services.yml :
# services.yml
# Declare the CommandHandler service
Your\Namespace\CommandHandler\ToggleProductStatusHandler:
autoconfigure: true
public: true
Then create two classes.
Command which provides the data needed in the CommandHandler.CommandHandler which will process the data.Create the Command
<?php
namespace Your\Namespace\Command;
/**
* Toggles the active status of a Product.
*/
final class ToggleProductStatusCommand
{
public function __construct(
private readonly int $productId,
private readonly bool $status
) {}
public function getProductId(): int
{
return $this->productId;
}
public function getStatus(): bool
{
return $this->status;
}
}
Create the CommandHandler
Now, create the CommandHandler which will execute it. Note that the class must have the AsCommandHandler attribute.
It’s also possible to do some validation and throw exceptions if needed.
<?php
namespace Your\Namespace\CommandHandler;
use PrestaShop\PrestaShop\Core\CommandBus\Attributes\AsCommandHandler;
use Your\Namespace\Command\ToggleProductStatusCommand;
use PrestaShopException;
use Product;
use Validate;
#[AsCommandHandler]
final class ToggleProductStatusHandler
{
public function handle(ToggleProductStatusCommand $command): void
{
$product = new Product($command->getProductId());
if (!Validate::isLoadedObject($product)) {
throw new PrestaShopException(sprintf('Product with id %d not found.', $command->getProductId()));
}
$product->active = $command->getStatus();
$product->save();
}
}
To execute the command, use the CommandBus service. The CommandBus automatically finds and executes the right handler thanks to the #[AsCommandHandler] attribute.
Here’s an example from a Symfony controller.
<?php
# Your controller
public function indexAction()
{
$productId = 1;
$status = false;
$command = new ToggleProductStatusCommand($productId, $status);
# Retrieves a prestashop.core.command_bus service
$commandBus = $this->getCommandBus();
$commandBus->handle($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;
In this example, we’ll create a Query to retrieve some information of a Product.
We’ll assume you’re creating it inside a module, which is the recommended way to extend PrestaShop.
If you’re not familiar with module creation yet, see how to create a module.
First declare the QueryHandler in the config/services.yml :
# services.yml
# Declare the QueryHandler service
Your\Namespace\QueryHandler\GetProductForEditingHandler:
autoconfigure: true
public: true
Then create three classes.
Query which will be composed of the data needed to retrieve the productQueryHandler which will retrieve the dataQueryResult class which represents the retrieved data in a structured object.Create the Query
<?php
namespace Your\Namespace\Query;
class GetProductForEditing
{
public function __construct(private readonly int $productId) {}
public function getProductId(): int
{
return $this->productId;
}
}
Create the QueryResult
This example will just retrieve the product id and its name.
<?php
namespace Your\Namespace\QueryResult;
class EditableProduct
{
public function __construct(
private readonly int $productId,
private readonly array $name
) {}
public function getProductId(): int
{
return $this->productId;
}
public function getName(): array
{
return $this->name;
}
}
Create the QueryHandler
Now, create the QueryHandler which will execute the Query and return an EditableProduct object. Note that the class must have the AsQueryHandler attribute.
It’s also possible to do some validation and throw exceptions if needed.
<?php
namespace Your\Namespace\QueryHandler;
use Your\Namespace\QueryResult\EditableProduct;
use Your\Namespace\Query\GetProductForEditing;
use PrestaShop\PrestaShop\Core\CommandBus\Attributes\AsQueryHandler;
use PrestaShopException;
use Product;
use Validate;
#[AsQueryHandler]
final class GetProductForEditingHandler
{
public function handle(GetProductForEditing $query): EditableProduct
{
$product = new Product($query->getProductId());
if (!Validate::isLoadedObject($product)) {
throw new PrestaShopException(sprintf('Product with id %d not found.', $query->getProductId()));
}
// Return an EditableProduct object containing the requested information
return new EditableProduct(
$product->id,
$product->name
);
}
}
Execute the Query
To execute the Query, use the QueryBus service. The QueryBus automatically finds and executes the right handler thanks to the #[AsQueryHandler] attribute.
Here’s an example from a Symfony controller.
<?php
# Your controller
public function indexAction()
{
# Retrieves a prestashop.core.query_bus service
$queryBus = $this->getQueryBus();
$query = new GetProductForEditing(1);
/** @var EditableProduct $editableProduct */
$editableProduct = $queryBus->handle($query);
}
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:
