Modern PrestaShop development is gradually moving away from putting all business logic directly inside controllers.
In older modules, updating a product often looked like this:
$product = new Product($idProduct);
$product->active = false;
$product->save();
This approach is easy to understand and still works in many situations. However, modern Symfony-based Back Office development increasingly relies on a different architectural pattern: CQRS.
CQRS stands for Command Query Responsibility Segregation.
The basic idea is simple:
-
Commands change data;
-
Queries retrieve data.
What is a Command?
A Command describes a specific action that should change the application state.
Examples include:
UpdateProductPriceCommand
ChangeOrderStatusCommand
DisableProductCommand
The Command itself does not update the database. It only contains the data required to perform the operation.
final class DisableProductCommand
{
public function __construct(
private int $productId
) {
}
public function getProductId(): int
{
return $this->productId;
}
}
The Command is then passed through the Command Bus:
$this->getCommandBus()->handle(
new DisableProductCommand($idProduct)
);
What does a Command Handler do?
The actual business logic is handled by a dedicated Handler.
final class DisableProductHandler
{
public function handle(
DisableProductCommand $command
): void {
$product = new Product(
$command->getProductId()
);
if (!Validate::isLoadedObject($product)) {
throw new PrestaShopException(
'Product not found'
);
}
$product->active = false;
$product->save();
}
}
The flow becomes:
Controller
↓
Command
↓
Command Bus
↓
Handler
↓
Product / Repository / Database
The controller no longer needs to know exactly how the product is updated.
What is a Query?
A Query follows a similar pattern, but it is used for retrieving data.
Examples:
GetProductForEditing
GetOrderForViewing
GetCategoryData
A controller can send a Query like this:
$productData = $this->getQueryBus()->handle(
new GetProductForEditing($idProduct)
);
The Query Handler retrieves the required information and returns the result.
The flow looks like this:
Controller
↓
Query
↓
Query Bus
↓
Query Handler
↓
Data
This keeps data retrieval separate from data modification.
Why does PrestaShop use CQRS?
One of the main reasons is the gradual migration of the Back Office to Symfony.
In the legacy architecture, a controller often worked directly with an ObjectModel:
Controller → Product → Database
In a more modern architecture, an additional layer is introduced:
Controller
↓
Command / Query
↓
Handler
↓
ObjectModel / Repository
This reduces the direct dependency between Symfony controllers and the way PrestaShop stores or modifies data.
It is especially useful in a large platform where legacy code, ObjectModel, Symfony and newer Domain components still coexist.
CQRS does not replace ObjectModel
Using CQRS does not mean that classes such as:
new Product();
new Order();
new Category();
are no longer allowed.
A Handler can still use the traditional ObjectModel layer.
The difference is where this logic is located.
Instead of:
Controller → Product
you get:
Controller
↓
UpdateProductCommand
↓
UpdateProductHandler
↓
Product ObjectModel
If the internal implementation changes later, the controller can remain unchanged.
Where CQRS is useful in PrestaShop modules
CQRS is not necessary for every small module.
If a module only displays an additional block through a hook, this architecture may introduce unnecessary complexity.
It becomes much more useful when a module includes:
-
a complex Back Office;
-
several Symfony controllers;
-
product imports;
-
external API synchronisation;
-
order management;
-
CLI commands;
-
cron jobs;
-
AJAX actions;
-
multiple business operations.
For example, the same stock synchronisation operation may be triggered from:
Back Office
CLI
Cron
API
Instead of maintaining four separate implementations, all entry points can execute the same Command:
UpdateProductStockCommand
↓
UpdateProductStockHandler
The business logic stays in one place.
A possible module structure
For a larger module, the structure can look like this:
src/
├── Controller/
├── Command/
├── CommandHandler/
├── Query/
├── QueryHandler/
├── DTO/
└── Exception/
For example:
Command/
UpdateProductPriceCommand.php
CommandHandler/
UpdateProductPriceHandler.php
Query/
GetProductData.php
QueryHandler/
GetProductDataHandler.php
This makes the codebase easier to navigate and maintain.
A common CQRS mistake
CQRS should not simply move a huge controller into a huge Handler.
A poor implementation might look like this:
public function handle($command)
{
// 500 lines of logic
}
If the operation is complex, the Handler should delegate responsibilities to dedicated services:
UpdateProductHandler
↓
ProductValidator
PriceCalculator
StockService
ExternalApiService
The purpose of CQRS is to separate responsibilities, not just create more files.
Should you use CQRS in your own module?
For a small module, probably not.
For a large module that will be maintained for several years, CQRS can be a very practical choice.
It is especially useful when the same business operation must be executed from different entry points.
A simple rule of thumb:
Simple hook → CQRS is probably unnecessary.
Large Symfony Back Office → CQRS is worth considering.
CLI + cron + API + Back Office → CQRS becomes especially useful.
Conclusion
CQRS in PrestaShop can be reduced to a very simple concept.
Changing data:
Controller
↓
Command
↓
Handler
Retrieving data:
Controller
↓
Query
↓
Handler
For developers, the key advantage is that business logic becomes independent from a specific controller.
This is particularly useful for modern PrestaShop modules with complex Back Office interfaces, imports, API integrations, CLI tools and background processes.
Understanding Commands, Queries and Handlers also makes the modern PrestaShop codebase much easier to read and extend.