Les bases du CQRS en Laravel

Publié le 23 octobre 2023 par Mathieu De Gracia
Couverture de l'article Les bases du CQRS en Laravel

Laravel propose par défaut une architecture de dossiers efficace pour les projets de petite à moyenne envergure, cependant, vous pourriez rapidement atteindre les limites de son modèle MVC dès lors que votre application gagnera en taille et en complexité.

Nous avons déjà abordé cette problématique par le passé en vous proposant notre vision d'une architecture modulaire en Laravel pouvant répondre à ces contraintes.

Le pattern CQRS, pour Command Query Responsibility Segregation, vise à séparer les opérations de lecture (queries) des opérations d'écriture (commands) de votre application.

Son principal objectif est d'améliorer la scalabilité et la flexibilité de l'application en traitant les requêtes de lecture et les commandes d'écriture de manière totalement indépendante.

Nous allons aborder dans cet article les bases du CQRS, principalement son vocabulaire, afin que vous soyez plus à l'aise pour parler de ce pattern !

Le CQRS est essentiellement constitué de 4 types de fichiers : la command, le commandHandler, les query, et le commandBus.

Essayons de définir les responsabilités de chacun de ces fichiers en implémentant une architecture CQRS basique dans une application Laravel gérant des articles de blog !

Command

Dans le pattern CQRS, la commande fait office de DTO, voici à quoi il ressemblerait dans notre exemple de création de Post :

1<?php
2 
3namespace App\Commands;
4 
5readonly class CreatePostCommand
6{
7 public function __construct(
8 public string $title,
9 public string $highlight,
10 public string $content,
11 ) {}
12}

Le nom "Command" est trompeur pour une application Laravel, cela n'est pas une commande Artisan !

Le DTO contient l'ensemble des informations que nous allons par la suite utiliser dans un commandHandler pour y effectuer des opérations, cela fait de lui un intermédiaire entre votre application et le handler contenant la logique métier.

Ce DTO encapsule et représente les informations de création d'un Post tout en étant capable de s'auto-valider, garantissant la cohérence et la validité des données transmises au handler.

La command agit donc comme une couche de transfert contribuant à la séparation des préoccupations.

Il est important de noter que l'objectif de ce DTO est de porter les intentions de la commande, il doit seulement transférer de l'information d'un composant à l'autre de votre application, sans contenir de logique métier, ce n'est pas un ValueObject !

Ce détail est extrêmement important car il est l'un des fondements du pattern CQRS en matière de séparation des responsabilités.

CommandHandler

Une commande sera toujours accompagnée de son handler, dans notre tutoriel, nous les placerons tous les deux au même niveau dans le dossier "app/Commands".

La responsabilité d'un handler est toute simple : contenir la logique métier d'une opération quelconque à partir des informations transmises par une commande.

1<?php
2 
3namespace App\Commands;
4 
5use App\Models\Post;
6use App\Commands\CreatePostCommand;
7 
8class CreatePostHandler
9{
10 public function __invoke(CreatePostCommand $command): void
11 {
12 Post::create([
13 'title' => $command->title,
14 'highlight' => $command->highlight,
15 'content' => $command->content,
16 ]);
17 }
18}

Dans une architecture plus classique, nous pourrions encapsuler cette logique de création de Post dans un service ayant une simple méthode "create", cependant, vous seriez contraint de transmettre autant d'arguments à cette méthode que le DTO possède de propriétés.

L'utilisation de la commande, le DTO, permet d'encapsuler ces arguments dans une classe et sera bien plus flexible qu'une longue liste d'arguments transmise à une méthode.

Les arguments ont une forte puissance conceptuelle dans votre application, il sera profitable d'utiliser un objet pour encapsuler cette complexité plutôt que de se laisser submerger par des arguments à mesure que les besoins de votre handler évolueront.

De par sa nature, le handler a donc une philosophie plus proche du pattern "action" que d'un véritable service.

Nous avons désormais notre commande et notre CommandHandler, il nous manque désormais l'intermédiaire entre les deux : le commandBus, qui orchestrera l'exécution des commandes de manière efficace et organisée !

CommandBus

La principale responsabilité du commandBus sera d'exécuter un handler.

Voici un exemple basique d'intégration d'un commandBus en Laravel mais parfaitement fonctionnel :

1<?php
2 
3namespace App;
4 
5use ReflectionClass;
6use Illuminate\Support\Str;
7use Psr\Container\ContainerInterface;
8 
9class CommandBus
10{
11 public function __construct(
12 private ContainerInterface $container,
13 ) {}
14 
15 public function handle($command): void
16 {
17 $handlerName = $this->resolveHandlerName($command);
18 
19 $handler = $this->container->get($handlerName);
20 
21 $handler($command);
22 }
23 
24 private function resolveHandlerName($command): string
25 {
26 $reflection = new ReflectionClass($command);
27 
28 $handlerName = Str::replaceLast('Command', 'Handler', $reflection->getShortName());
29 
30 $namespaceName = $reflection->getNamespaceName();
31 
32 return $namespaceName . '\\' . $handlerName;
33 }
34}

À partir d'une commande, le commandBus est en mesure d'instancier le handler associé depuis le conteneur de Laravel et de l'exécuter.

Ce commandBus est très simpliste mais constitue une première approche accessible si vous voulez tester l'utilisation du pattern CQRS dans votre application Laravel.

Un détail important, le CommandBus n'a pas de valeur de retour, ce n'est pas dans sa philosophie.

Si ce besoin se fait ressentir vous devrez alors tourner votre attention vers un mécanisme de retour "indirect" en utilisant par exemple des événements ou des jobs, mais nous dépassons ici le simple cas de ce tutoriel.

Un commandBus plus élaboré pourrait également gérer de la concurrence, mettre en place des transactions, voir même implémenter un système de logs des commandes entrantes.

Des actions de pré et post traitement d'une commande pourraient également être envisagées afin d'effectuer des actions similaires à un middleware sur la commande.

Un commandBus est une gare de triage, aiguillant aux besoins vos commandes, à vous de créer le commandBus répondant au mieux à vos besoins !

Queries

Nous nous sommes concentrés jusqu'à présent sur la persistance des données, le dernier élément important du pattern CQRS concerne l'accès aux informations : la query.

Les queries sont conçues pour interroger votre source de données sans provoquer d'effet de bord, nous placerons nos queries dans un dossier "app/Queries".

1<?php
2 
3namespace App\Queries;
4 
5use App\Models\Post;
6 
7class GetPostDetailsQuery
8{
9 public function __construct(
10 private int $postId,
11 ) {}
12 
13 public function get(): array
14 {
15 $post = Post::query()
16 ->select('title', 'highlight', 'content')
17 ->findOrFail($this->postId);
18 
19 return $post->toArray();
20 }
21}

Attention, la query n'est pas à proprement parler un repository, sa seule responsabilité doit être d'accéder aux sources de données et non d'y effectuer de la persistance !

Néanmoins, nous retrouvons certains avantages des repositories, comme la possibilité d'extraire et d'encapsuler des requêtes dans une classe distincte, améliorant ainsi la testabilité de votre application !

Assemblons le tout

Nous sommes désormais en mesure de créer un controller exploitant notre pattern CQRS !

1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\CommandBus;
6use Illuminate\Http\Request;
7use App\Commands\CreatePostCommand;
8use App\Queries\GetPostDetailsQuery;
9 
10class PostController extends Controller
11{
12 public function __construct(
13 private CommandBus $commandBus,
14 ) {}
15 
16 public function show(int $postId)
17 {
18 $query = new GetPostDetailsQuery($postId);
19 
20 return response()->json($query->get());
21 }
22 
23 public function store(Request $request)
24 {
25 $command = new CreatePostCommand(
26 title: $request->get('title'),
27 highlight: $request->get('highlight'),
28 content: $request->get('content'),
29 );
30 
31 $this->commandBus->handle($command);
32 
33 return response()->noContent();
34 }
35}

Conclusion

En résumé, cet article présente succinctement les bases du pattern CQRS en se concentrant sur son vocabulaire essentiel.

Le CQRS, comme toute forme d’architecture logicielle, ne doit pas être une solution systématique, il impose de fortes contraintes qui doivent être comprises et acceptées avant d'être intégré dans votre projet.

Pour autant, comme vous venez de le voir, le pattern est relativement simple à exploiter dans sa forme basique et offre une solution intéressante pour découpler et mieux encapsuler les briques fonctionnelles de votre application !

Source : https://github.com/laravel-fr/support-cqrs
Mathieu De Gracia avatar
Mathieu De Gracia
Des fois, mon chat code à ma place 🐱

A lire

Autres articles de la même catégorie