Aller au contenu principal
Version: 5.x

đŸ€  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 retrieve(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.