Principe SEC avec Laravel

Prenons un module simple chargé d'ajouter de nouveaux utilisateurs.

Et par son exemple, nous verrons quelles possibilités ouvre l'application du principe DRY.

Pour moi, le principe de DRY (Don't Repeat Yourself) a toujours été incarné dans deux définitions de base:

  1. La duplication des connaissances est toujours une violation du principe
  2. La duplication de code n'est pas toujours une violation du principe

Commençons par les contrôleurs contenant le minimum de logique.

class UserController
{
    public function create(CreateRequest $request)
    {
        $user = User::create($request->all());
        
        return view('user.created', compact('user'));
    }
}

class UserApiController
{
    public function create(CreateRequest $request)
    {
        $user = User::create($request->all());
        
        return response()->noContent(201);
    }
}

Au stade initial, une telle répétition du code semble plutôt inoffensive.
Mais nous avons déjà une duplication des connaissances, et la duplication des connaissances est interdite.
Pour ce faire, nous généralisons la création d'utilisateurs dans la classe UserService

class UserService
{
    public function create(array $data): User
    {
        $user = new User;
        $user->email = $data['email'];
        $user->password = $data['password'];
        $user->save();

        return $user;
    }
    
    public function delete($userId): bool 
    {
        $user = User::findOrFail($userId);
 
        return $user->delete();
    }    
}

Après avoir déplacé toute la logique de travail avec le modèle vers le service, nous nous débarrassons de sa duplication dans le contrôleur. Mais nous avons un autre problème. Disons que nous devons compliquer un peu le processus de création d'un utilisateur.

class UserService
{
    protected $blogService;
    
    public function __construct(BlogService $blogService)
    {
        $this->blogService = $blogService;
    }

    public function create(array $data): User
    {
        $user = new User;
        $user->email = $data['email'];
        $user->password = $data['password'];
        $user->save();
        
        $blog = $this->blogService->create();
        $user->blogs()->attach($blog);

        return $user;
    }
    
    //Other methods
}

Progressivement, la classe UserService commencera à se développer et nous courons le risque d'obtenir une super classe avec un grand nombre de dépendances.

Classe d'action unique CreateUser


Afin d'éviter de telles conséquences, vous pouvez diviser le service en classes d'une seule action.

Exigences de base pour cette classe:

  • Nom représentant l'action à effectuer
  • A une seule méthode publique (j'utiliserai la méthode magique __invoke )
  • A toutes les dépendances nécessaires
  • Assure le respect de toutes les règles métier en lui-même, génère une exception lorsqu'elles sont violées

class CreateUser
{
    protected $blogService;

    public function __construct(BlogService $blogService)
    {
        $this->blogService = $blogService;
    }

    public function __invoke(array $data): User
    {
        $email = $data['email'];
        
        if (User::whereEmail($email)->first()) {
            throw new EmailNotUniqueException("$email should be unique!");
        }
        
        $user = new User;
        $user->email = $data['email'];
        $user->password = $data['password'];
        $user->save();

        $blog = $this->blogService->create();
        $user->blogs()->attach($blog);

        return $user;
    }
}

Nous avons déjà une vérification du champ e-mail dans la classe CreateRequet, mais il est logique d'ajouter une vérification ici également. Cela reflète plus précisément la logique métier de création d'un utilisateur et simplifie également le débogage.

Les contrôleurs prennent la forme suivante

class UserController
{
    public function create(CreateRequest $request, CreateUser $createUser)
    {
        $user = $createUser($request->all());

        return view('user.created', compact('user'));
    }
}

class UserApiController
{
    public function create(CreateRequest $request, CreateUser $createUser)
    {
        $user = $createUser($request->all());

        return response()->noContent(201);
    }
}

En conséquence, nous avons une logique de création d'utilisateur complètement isolée. Il est pratique de modifier et d'étendre.

Voyons maintenant quels avantages cette approche nous offre.

Par exemple, il existe une tâche pour importer des utilisateurs.

class ImportUser
{
    protected $createUser;
    
    public function __construct(CreateUser $createUser)
    {
        $this->createUser = $createUser;
    }
    
    public function handle(array $rows): Collection
    {
        return collect($rows)->map(function (array $row) {
            try {
                return $this->createUser($row);
            } catch (EmailNotUniqueException $e) {
                // Deal with duplicate users
            }
        });
    }
}

Nous avons la possibilité de réutiliser le code en l'intégrant dans la méthode Collection :: map (). Et aussi traiter pour nos besoins les utilisateurs dont les adresses e-mail ne sont pas uniques.

Pansement


Supposons que nous devons enregistrer chaque nouvel utilisateur dans un fichier.

Pour ce faire, nous n'incorporerons pas cette action dans la classe CreateUser elle-même, mais utiliserons le modèle Decorator.

class LogCreateUser extends CreateUser 
{
    public function __invoke(array $data)
    {
        Log::info("A new user has registered: " . $data['email']);
        
        parent::__invoke($data);
    }
}

Ensuite, en utilisant le conteneur Laravel IoC , nous pouvons associer la classe LogCreateUser à la classe CreateUser , et la première sera implémentée chaque fois que nous aurons besoin d'une instance de la seconde.

class AppServiceProvider extends ServiceProvider
{

    // ...

    public function register()
    {
        $this->app->bind(CreateUser::class, LogCreateUser::class);
    }

Nous avons également la possibilité de définir le paramètre de création d'utilisateur à l'aide d'une variable dans le fichier de configuration.

class AppServiceProvider extends ServiceProvider
{

    // ...

    public function register()
    {
         if (config("users.log_registration")) {
             $this->app->bind(CreateUser::class, LogCreateUser::class);
         }
    }

Conclusion


Voici un exemple simple. Les avantages réels commencent à apparaître à mesure que la complexité augmente. Nous savons toujours que le code est au même endroit et ses limites sont clairement définies.

Nous obtenons les avantages suivants: empêche la duplication, simplifie les tests
et ouvre la voie à l'application d'autres principes et modèles de conception.

All Articles