Blog navigation

Blog Rss rss_feed

CQRS in PrestaShop 9: Commands, Queries and Modern Module Architecture

CQRS in PrestaShop 9: Commands, Queries and Modern Module Architecture

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.

Was this blog post helpful to you?

    
👈 Присоединяйтесь к нашему Telegram-каналу!

Будьте в курсе последних новинок и фишек e-commerce: советы, полезные инструменты и эксклюзивные материалы.

No comments at this moment
close

Checkout

close

Favourites

Promo