Digital solutions
Design Patterns avec Laravel

Chain of Responsibility Pattern

  • Type : Comportemental
  • Difficulté : 6/10
  • Définition succincte : Le Chain of Responsibility Pattern est un design pattern comportemental qui permet à plusieurs objets de traiter une requête de manière séquentielle. Chaque objet, appelé un handler, a la possibilité de traiter la requête ou de la transmettre au prochain objet dans la chaîne. Ce pattern permet de découpler les émetteurs de requêtes de leurs gestionnaires, offrant ainsi une grande flexibilité dans la gestion des responsabilités.
  • Contexte spécifique à Laravel : En Laravel, ce pattern est utilisé dans le système des middlewares, où une requête HTTP passe à travers une chaîne de middlewares qui peuvent la modifier ou la vérifier avant de la transmettre à la prochaine étape du pipeline.

Objectif du Chain of Responsibility Pattern

Le but principal de ce pattern est de permettre à plusieurs objets de traiter ou d'ignorer une requête de manière séquentielle sans que l'émetteur de la requête sache quel objet finira par la traiter. Chaque handler dans la chaîne peut décider de prendre en charge la requête ou de la passer au handler suivant. Ce modèle est idéal pour les situations où plusieurs étapes indépendantes doivent être appliquées à une requête.

Structure du Chain of Responsibility Pattern

  • Handler : Interface ou classe abstraite qui déclare une méthode pour gérer les requêtes et définir le prochain handler dans la chaîne.
  • ConcreteHandler : Classe concrète qui implémente la gestion d'une requête spécifique et décide de la passer ou non à l'étape suivante.
  • Client : Le code qui crée la chaîne de handlers et envoie la requête au premier handler.

1. Exemple d'utilisation dans Laravel

Dans Laravel, les exceptions peuvent être gérées en chaîne dans le fichier app/Exceptions/Handler.php. Chaque exception peut être capturée par un gestionnaire approprié ou propagée à l'étape suivante.

Exemple : Gestionnaire d'exception dans Laravel

// app/Exceptions/Handler.php

namespace App\Exceptions;

use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Validation\ValidationException;
use Illuminate\Auth\AuthenticationException;

class Handler extends ExceptionHandler
{
    public function render($request, \Throwable $exception)
    {
        if ($exception instanceof ValidationException) {
            return response()->json(['error' => 'Validation failed'], 422);
        }

        if ($exception instanceof AuthenticationException) {
            return response()->json(['error' => 'Authentication failed'], 401);
        }

        // Passer l'exception à l'étape suivante
        return parent::render($request, $exception);
    }
}

Ici, le gestionnaire d'exception Handler intercepte les exceptions de validation et d'authentification. Si l'exception ne correspond à aucun de ces cas, elle est transmise au gestionnaire suivant dans la chaîne (via parent::render).

2. Implémentation complète dans Laravel

Pour illustrer pleinement le Chain of Responsibility Pattern, imaginons un scénario où nous avons plusieurs étapes de traitement d'une requête, telles que la validation, l'authentification, et le traitement de la requête. Chaque étape sera implémentée comme un handler dans la chaîne de responsabilité.

a) Créer l'interface Handler (classe de base pour la chaîne)

Commençons par définir l'interface Handler, qui permet à chaque étape de gérer une requête ou de la transmettre à l'étape suivante.

// app/Contracts/Handler.php

namespace App\Contracts;

interface Handler
{
    public function setNext(Handler $handler): Handler;
    public function handle($request): ?string;
}

b) Implémenter un AbstractHandler

Chaque maillon de la chaîne doit hériter d'une classe de base commune. Cela permet à chaque maillon de traiter la requête ou de la transmettre au suivant dans la chaîne. Nous allons donc créer une classe abstraite qui implémente certaines fonctionnalités de base, comme la gestion du prochain handler dans la chaîne.

// app/Handlers/AbstractHandler.php

namespace App\Handlers;

use App\Contracts\Handler;

abstract class AbstractHandler implements Handler
{
    protected $nextHandler;

    public function setNext(Handler $handler): Handler
    {
        $this->nextHandler = $handler;
        return $handler;
    }

    public function handle($request): ?string
    {
        if ($this->nextHandler) {
            return $this->nextHandler->handle($request);
        }

        return null;
    }
}

c) Créer des ConcreteHandlers (les différents maillons de la chaîne)

Nous allons créer trois handlers concrets : un pour valider la requête, un pour authentifier l'utilisateur, et un pour traiter la requête. Chaque validation hérite de AbstractHandler. Si la validation échoue, la chaîne s'arrête. Sinon, la requête est transmise au maillon suivant.

1. Handler ValidateRequest

Ce handler vérifie que l'email est présent dans la requête.

// app/Handlers/ValidateRequest.php

namespace App\Handlers;

use App\Contracts\Handler;
use Illuminate\Validation\ValidationException;

class ValidateRequest extends AbstractHandler
{
    public function handle($request): ?string
    {
        if (empty($request['email'])) {
            throw ValidationException::withMessages(['email' => 'Email is required.']);
        }

        return parent::handle($request);
    }
}
2. Handler AuthenticateUser

Ce handler vérifie que l'email correspond à un utilisateur authentifié.

// app/Handlers/AuthenticateUser.php

namespace App\Handlers;

use App\Contracts\Handler;
use Illuminate\Auth\AuthenticationException;

class AuthenticateUser extends AbstractHandler
{
    public function handle($request): ?string
    {
        if ($request['email'] !== 'admin@example.com') {
            throw new AuthenticationException('User not authenticated.');
        }

        return parent::handle($request);
    }
}
3. Handler ProcessRequest

Ce handler final traite la requête.

// app/Handlers/ProcessRequest.php

namespace App\Handlers;

class ProcessRequest extends AbstractHandler
{
    public function handle($request): ?string
    {
        return 'Request processed for user: ' . $request['email'];
    }
}

d) Utiliser le Chain of Responsibility Pattern dans une commande CLI

Nous allons maintenant encapsuler ces handlers dans une commande CLI pour simuler le traitement d'une requête.

// app/Console/Commands/ProcessUserRequest.php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Handlers\ValidateRequest;
use App\Handlers\AuthenticateUser;
use App\Handlers\ProcessRequest;
use Illuminate\Validation\ValidationException;
use Illuminate\Auth\AuthenticationException;

class ProcessUserRequest extends Command
{
    protected $signature = 'request:process';
    protected $description = 'Process user request through chain of responsibility';

    public function handle()
    {
        // Créer les handlers
        $validateRequest = new ValidateRequest();
        $authenticateUser = new AuthenticateUser();
        $processRequest = new ProcessRequest();

        // Construire la chaîne
        $validateRequest->setNext($authenticateUser)->setNext($processRequest);

        // Simuler une requête utilisateur
        $request = ['email' => 'admin@example.com'];

        try {
            // Traiter la requête à travers la chaîne
            $result = $validateRequest->handle($request);
            $this->info($result);
        } catch (ValidationException $e) {
            $this->error('Validation failed: ' . $e->getMessage());
        } catch (AuthenticationException $e) {
            $this->error('Authentication failed: ' . $e->getMessage());
        } catch (\Exception $e) {
            $this->error('An error occurred: ' . $e->getMessage());
        }
    }
}

e) Exécution de la commande

Tu peux maintenant exécuter cette commande pour voir comment chaque étape dans la chaîne de responsabilité gère la requête.

php artisan request:process

Scénarios possibles

  • Si l'email est manquant, la validation échoue et une exception est levée :
  Validation failed: Email is required.
  • Si l'utilisateur n'est pas authentifié, une exception d'authentification est levée :
  Authentication failed: User not authenticated.
  • Si tout est correct, la requête est traitée avec succès :
  Request processed for user: admin@example.com

Avantages du Chain of Responsibility Pattern

  1. Séparation des préoccupations : Chaque étape de la validation est encapsulée dans sa propre classe, ce qui rend le code plus modulaire et plus facile à maintenir.
  2. Extensibilité : Il est facile d'ajouter ou de retirer des étapes dans la chaîne sans modifier le comportement des autres maillons.
  3. Réutilisabilité : Les maillons de la chaîne peuvent être réutilisés dans d'autres parties de l'application, ou même dans d'autres chaînes.
  4. Flexibilité : La chaîne peut être configurée dynamiquement en fonction des besoins (par exemple, certaines validations peuvent être désactivées dans certains cas).
  5. Élimination des conditions complexes : Le pattern élimine les longues séquences de conditions if-else qui peuvent rendre le code difficile à lire et à maintenir.

Inconvénients du Chain of Responsibility Pattern

  1. Difficile à déboguer : Lorsque la requête est interrompue dans la chaîne, il peut être difficile de savoir quel handler a bloqué la requête.
  2. Dépendance à l'ordre : L'ordre des handlers est important, et une mauvaise organisation peut entraîner des comportements inattendus.
  3. Performances : Si la chaîne est trop longue ou comporte trop de handlers, cela peut affecter la performance du système.

Conclusion

Le Chain of Responsibility Pattern est très efficace pour gérer des processus complexes qui nécessitent plusieurs étapes indépendantes. En Laravel, il est utilisé dans les middlewares pour traiter les requêtes HTTP de manière séquentielle, mais il peut aussi être appliqué à d'autres scénarios où des actions doivent être réalisées en chaîne. Ce pattern favorise la modularité, la flexibilité et l'extensibilité dans les applications.