Newsletter Apprendre Laravel #10

Envoyée le 02 mai 2019

Aujourd'hui j'aimerais discuter d'une architecture très commune dans les applications web : les events listeners.

Un exemple d'event listener

Prenons l'exemple d'une personne qui doit recevoir un mail lors de son inscription à un site. La solution la plus simple consiste à envoyer le mail directement dans le contrôleur responsable de l'inscription.

<?php

class RegistrationController
{
    public function register()
    {
        // ... register user

        Mail::to($user->email)->send(new WelcomeEmail($user));
    }
}

La deuxième solution consiste à utiliser un système d'event listeners :

<?php

class RegistrationController
{
    public function register()
    {
        // ... register user

        event(new UserWasRegisteredEvent($user));
    }
}
<?php

class UserWasRegisteredEvent
{
    public $user;

    public function __construct(User $user)
    {
        $this->user = $user;
    }
}
<?php

class SendWelcomeEmailListener
{
    public function handle(UserWasRegisteredEvent $event)
    {
        Mail::to($event->user->email)->send(new WelcomeEmail($event->user));
    }
}

Quels sont les avantages et les inconvénients d'une telle architecture ?

Le contrôleur n'a maintenant plus conscience de l'envoi du mail. Ce point peut-être vu comme un avantage, mais je le perçois habituellement comme un désavantage important. Le problème est que pour avoir une vision complète de ce qui est exécuté lors de l'inscription d'une personne nous devons maintenant lire le contrôleur et trouver tous les listeners appelés lors de l'évènement UserWasRegisteredEvent (soit via une recherche totale dans l'application dans le cas de l'auto-discovery, ou alors via le EventServiceProvider).

Nous pouvons également ajouter de nouvelles actions à réaliser lors de l'inscription d'un nouvel utilisateur sans avoir à modifier le RegistrationController. Mais en contre partie nous devons créer un listener pour chaque nouvelle action : une nouvelle classe ou une nouvelle ligne dans un fichier existant ? Certaines personnes pensent qu'il est préférable d'ajouter de nouveaux fichiers plutôt que de modifier les fichiers existants (le principe du Open-Closed dans l'acronyme SOLID). Je ne suis plus de cet avis-là après avoir vu Jonathan Blow travailler sur ses fichiers de 7000 lignes de code. J'essaye actuellement de réduire le nombre de fichiers dans mes applications afin de réduire le ratio bruit/fonctionnalité. Lorsque nous créons un nouveau fichier comme le SendWelcomeEmailListener plus haut, nous avons au moins 10 lignes techniques pour 1 seule ligne de code. En ayant des classes avec plus de fonctionnalités et non pas juste un alias vers une autre partie de l'application, nous pouvons réduire ce ratio et donc le bruit dans notre code. Vous pouvez utiliser la commande find app -type f -exec wc -l {} + | sort -rn pour lister les fichiers de votre application ainsi que le nombre de le lignes de code qu'ils contiennent. Souvent nous cherchons les fichiers les plus gros pour essayer de les découper, mais nous pouvons également chercher les plus petits pour essayer de les supprimer.

Enfin le dernier avantage, souvent évoqué pour encourager l'utilisation des events listeners, est de ne pas surcharger la fonction register du contrôleur. Cela dépend beaucoup du nombre de listeners que vous avez. Dans l'exemple, avec un seul listener, nous voyons que cela ne change rien à la complexité du contrôleur. C'est une perte sèche d'une vingtaine de nouvelles lignes de code. Et cela ne va pas en s'améliorant, car nous devons toujours créer de nouveau listeners pour chaque nouvelle action. Par contre, si le listener réalise vraiment lui-même l'action, et que nous ne voulons pas utiliser le système d'event listeners, nous allons devoir mettre ces lignes de code autre part (dans une fonction privée du contrôleur, dans un modèle, dans une classe PHP existante ou nouvelle…). Je préfère habituellement la lisibilité de 3, 4, 5 (ou plus) appels de fonctions (services, jobs, mails…) dans mon contrôleur les uns après les autres que d'avoir cette liste éclatée dans différents listeners.

Quand utiliser un système d'event listeners ?

J'aime toujours penser que si un système existe, il doit être utile quelque part. Les personnes ayant conçu ce système et beaucoup de personnes utilisant ce système sont sans doute plus intelligentes que moi.

La princpale raison d'utiliser cette architecture c'est lorsque nous ne pouvons techniquement pas modifier le code qui réalise l'action initiale, par exemple lors de l'utilisation d'une bibliothèque externe. spatie/backups nous fournit un système d'évènements à écouter pour pouvoir réagir à notre façon lorsque la création d'une sauvegarde échoue, lors d'un succès, avant le début d'une sauvegarde… Dans ce cas, les event listeners sont totalement adaptés car nous ne pouvons pas modifier le code de Spatie. Pensez-y si vous développez un jour une bibliothèque, rien de plus embêtant que de ne pas pouvoir réagir car le code n'est pas modifiable.

La deuxième raison qui me vient à l'esprit c'est la même que précédemment mais dans nos propres applications. Si votre application est d'une certaine taille, avec des équipes différentes, peut-être des projets Git différents, les évènements peuvent permettre à une autre équipe de développeur de réagir à une action du système sans avoir forcément besoin de demander une modification à l'équipe initiale. Notez quand même que c'est un cas qui n'est pas courant dans les projets Laravel d'une taille normale.

Comme d'habitude, je ne connais pas tout et peut-être que des utilisations géniales m'ont échappé, n'hésitez pas à venir sur le Discord en discuter ou en réponse à ce mail.

Bonne fin de semaine à tous,

Thibaud