Aller au contenu principal
Version: Canary 🚧

Séparation des responsabilités des requêtes de commande (CQRS)

Introduction

CQRS (Command Query Responsibility Segregation) est un modèle d'architecture qui sépare les responsabilités des commandes et des requêtes. L'idée principale est d'utiliser un modèle différent pour les opérations d'écriture (commandes) et de lecture (requêtes).

Quand utiliser CQRS ?

Cette approche peut être utile dans des situations complexes, bien que pour la plupart des systèmes, CQRS puisse introduire une certaine complexité supplémentaire. Privilégiez CQRS pour les systèmes où les besoins en lecture et en écriture sont très différents.

Pour plus d'informations sur CQRS, consultez cet article de Martin Fowler.

Installation

Ajoutez le package CQRS à votre projet avec la commande suivante :

composer require bowphp/cqrs

Utilisation des commandes (Commands)

Les commandes représentent des intentions d'effectuer des opérations d'écriture ou des actions qui modifient l'état de votre système.

Étape 1 : Créer une commande

Créez une commande en implémentant l'interface Bow\CQRS\Command\CommandInterface. Par exemple, pour créer un utilisateur :

app/Commands/CreateUserCommand.php
use Bow\CQRS\Command\CommandInterface;

class CreateUserCommand implements CommandInterface
{
public function __construct(
public string $username,
public string $email
) {}
}

Étape 2 : Créer un handler pour la commande

Créez un handler qui exécutera la logique associée à la commande. Ce handler doit implémenter l'interface Bow\CQRS\Command\CommandHandlerInterface :

app/CommandHandlers/CreateUserCommandHandler.php
use Bow\CQRS\Command\CommandHandlerInterface;

class CreateUserCommandHandler implements CommandHandlerInterface
{
public function __construct(public UserService $userService) {}

public function process(CommandInterface $command): mixed
{
if ($this->userService->exists($command->email)) {
throw new UserServiceException(
"The user already exists"
);
}

return $this->userService->create([
"username" => $command->username,
"email" => $command->email
]);
}
}
Bonnes pratiques

Le handler doit contenir la logique métier associée à la commande. Il est recommandé d'injecter les dépendances (comme les services) via le constructeur.

Étape 3 : Enregistrer la commande et son handler

Ajoutez la commande et son handler dans le registre des commandes dans App\Configurations\ApplicationConfiguration::class :

app/Configurations/ApplicationConfiguration.php
use Bow\CQRS\Registration as CQRSRegistration;

public function run()
{
CQRSRegistration::commands([
CreateUserCommand::class => CreateUserCommandHandler::class
]);
}

Étape 4 : Exécuter la commande dans un contrôleur

Dans un contrôleur, utilisez le CommandBus pour exécuter la commande :

app/Controllers/UserController.php
namespace App\Controllers;

use Bow\CQRS\Command\CommandBus;
use App\Controllers\Controller;
use App\Commands\CreateUserCommand;

class UserController extends Controller
{
public function __construct(private CommandBus $commandBus) {}

public function __invoke(Request $request)
{
$payload = $request->only(['username', 'email']);

$command = new CreateUserCommand(
$payload['username'],
$payload['email']
);

$result = $this->commandBus->execute($command);

return redirect()
->back()
->withFlash("message", "User created");
}
}

Ajoutez une route pour appeler cette méthode :

$app->post("/users/create", UserController::class);

Utilisation des requêtes (Queries)

Les requêtes sont utilisées pour récupérer des données sans modifier l'état de votre système. Elles doivent être en lecture seule.

Étape 1 : Créer une requête

Créez une requête en implémentant l'interface Bow\CQRS\Query\QueryInterface. Par exemple, pour récupérer un utilisateur par son ID :

app/Queries/GetUserByIdQuery.php
use Bow\CQRS\Query\QueryInterface;

class GetUserByIdQuery implements QueryInterface
{
public function __construct(public int $userId) {}
}

Étape 2 : Créer un handler pour la requête

Créez un handler pour exécuter la logique de récupération des données. Ce handler doit implémenter l'interface Bow\CQRS\Query\QueryHandlerInterface :

app/QueryHandlers/GetUserByIdQueryHandler.php
use Bow\CQRS\Query\QueryHandlerInterface;

class GetUserByIdQueryHandler implements QueryHandlerInterface
{
public function __construct(public UserService $userService) {}

public function process(QueryInterface $query): mixed
{
return $this->userService->findById($query->userId);
}
}
Lecture seule

Les handlers de requête ne doivent jamais modifier l'état du système. Ils sont exclusivement dédiés à la récupération de données.

Étape 3 : Enregistrer la requête et son handler

Ajoutez la requête et son handler dans le registre des requêtes dans App\Configurations\ApplicationConfiguration::class :

app/Configurations/ApplicationConfiguration.php
use Bow\CQRS\Registration as CQRSRegistration;

public function run()
{
CQRSRegistration::queries([
GetUserByIdQuery::class => GetUserByIdQueryHandler::class
]);
}

Étape 4 : Exécuter la requête dans un contrôleur

Dans un contrôleur, utilisez le QueryBus pour exécuter la requête :

app/Controllers/UserController.php
namespace App\Controllers;

use Bow\CQRS\Query\QueryBus;
use App\Controllers\Controller;
use App\Queries\GetUserByIdQuery;

class UserController extends Controller
{
public function __construct(private QueryBus $queryBus) {}

public function show(int $userId)
{
$query = new GetUserByIdQuery($userId);

$user = $this->queryBus->execute($query);

return view("user.profile", ["user" => $user]);
}
}

Ajoutez une route pour appeler cette méthode :

$app->get("/users/{id}", [UserController::class, 'show']);

Enregistrement avec les attributs PHP

Vous pouvez aussi lier automatiquement les handlers à leurs commandes/requêtes avec des attributs PHP et une seule déclaration de registre.

app/CommandHandlers/CreateUserCommandHandler.php
use Bow\CQRS\Attribute\CommandHandler;
use Bow\CQRS\Command\CommandHandlerInterface;
use Bow\CQRS\Command\CommandInterface;

#[CommandHandler(CreateUserCommand::class)]
class CreateUserCommandHandler implements CommandHandlerInterface
{
public function __construct(public UserService $userService) {}

public function process(CommandInterface $command): mixed
{
// ...
}
}
app/QueryHandlers/GetUserByIdQueryHandler.php
use Bow\CQRS\Attribute\QueryHandler;
use Bow\CQRS\Query\QueryHandlerInterface;
use Bow\CQRS\Query\QueryInterface;

#[QueryHandler(GetUserByIdQuery::class)]
class GetUserByIdQueryHandler implements QueryHandlerInterface
{
public function __construct(public UserService $userService) {}

public function process(QueryInterface $query): mixed
{
// ...
}
}

Enregistrez ensuite tous les handlers annotés en une fois :

app/Configurations/ApplicationConfiguration.php
use Bow\CQRS\Registration as CQRSRegistration;

public function run()
{
CQRSRegistration::handlers([
CreateUserCommandHandler::class,
GetUserByIdQueryHandler::class,
]);
}

Conclusion

Avec CQRS, vous pouvez structurer vos applications pour séparer clairement les préoccupations entre la lecture et l'écriture, tout en conservant un code organisé et maintenable.

Avantages de CQRS
  • Séparation des responsabilités : Les opérations de lecture et d'écriture sont isolées
  • Scalabilité : Possibilité d'optimiser indépendamment les lectures et les écritures
  • Maintenabilité : Code plus facile à tester et à maintenir
  • Flexibilité : Permet d'utiliser différents modèles de données pour les lectures et les écritures

Il manque quelque chose ?

Si vous rencontrez des problèmes avec la documentation ou si vous avez des suggestions pour améliorer la documentation ou le projet en général, veuillez déposer une issue pour nous, ou envoyer un tweet mentionnant le compte Twitter @bowframework ou sur directement sur le github.