Messenger Symfony : Responsabilité unique dans vos handlers

Quand on écrit du code, on découpe les fonctionnalités et les classes pour que celles-ci ne fassent qu’une seule chose à la fois.

Le risque de ne pas suivre ce principe c’est de se retrouver avec une classe qui gère trop de choses à la fois.

Un exemple réel

En tant qu’admin je veux envoyer une notification aux utilisateurs d’une application mobile lorsque j’upload un fichier dans un de leur dossier, seulement si ce dossier possède le tag public.

On peut écrire le test en premier lieu :

Et le code :

Il manque quelque chose dans le code, un pré-requis pourtant demandé dans les specs. Il manque la vérification du tag public.

Si on utilise ce code dans cet état, et qu’on upload deux fichiers, le client risque de venir vous voir en disant :

Pourtant le code ci-dessus devrait rester tel quel.

Pourquoi ? Et bien la responsabilité de ce handler c’est de récupérer un UploadedFile et de créer des notifications. Le reste c’est des conditions qui n’ont pas leur place ici.

Si vous ne voulez pas envoyer de notification, il suffit de ne pas envoyer le message.

Le problème qui consiste à ajouter des responsabilités dans un handler c’est qu’on risque de faire des erreurs et de rendre le code moins robuste.

Par exemple si on ajoutait la condition sur le tag public :

Vous allez me dire que c’est pas grand chose à ajouter.

Seulement en ouvrant la porte à ce genre d’exception, on se laisse vite submerger.

1 an plus tard

Après plusieurs retours du client et évolution des besoins :

J’aurai pu ajouter encore plein de cas à l’intérieur…

Quand on voit le code à la base, et 1 an plus tard. Le constat est clair : complexité et maintenabilité du code en très forte augmentation !

Alors que faire pour gérer tous les cas 1 an plus tard ?

Si ça avait été moi, je n’aurai pas touché au handler de base.

Le premier test de sécurité pour vérifier que l’utilisateur est un modérateur ne doit pas être fait ici. On ne va pas vérifier à chaque fois l’utilisateur connecté sur tous nos handlers, ça se fait en amont dans les autorisations, surtout qu’on pourrait exécuter ce handler via une tâche cron sans utilisateur..

Les autres tests sur l’objet UploadedFile pour vérifier s’il a le bon tag, le bon user en admin sur son dossier, ou bien si il y a une validation custom….Tout ça également doit être fait en amont. Trop de cas peuvent se présenter, trop de changements. Et le changement ça peut bloquer l’envie de toucher au code.

Le dernier cas, qui doit envoyer un email avec en pièce jointe le fichier si c’est un pdf ou un odt, je passerai par un event et un handler dédié. Par exemple :

Ce listener récupère un event, regarde le type de fichier, et dispatch un nouveau message. On ne fait pas de traitement dans un EventListener. Pourquoi ? Parce que ça n’est pas sa fonctionnalité. Il est juste là pour récupérer des events et créer des messages.

Pour la documentation sur l’eventBus c’est par ici.

Pour le DispatchAfterCurrentBusStamp, ça permet de dire au MessageBus de ne traiter l’event dans le listener seulement si le traitement du premier handler est complet et sans exception. Comme ça on s’assure que les notifications sont bien créées avant de passer à la suite. Voir la documentation.

Conclusion

En découpant votre code par fonctionnalité et non par spec de A à Z, on en revient aux bases : une classe, une fonctionnalité.

On a ainsi des handlers qui font peu de choses, et qui sont réutilisables dans d’autres contextes. Si on devait envoyer des notifications via une tâche cron, on aurait juste à appeler le handler sans se poser de question, ni à se retrouver bloqué parce qu’on n’a pas d’utilisateur connecté.

Pour aller plus loin sur les autorisations dans les handlers

Doit-on sécuriser chaque handler ?

C’est comme avoir une clef pour entrer dans une pièce dans laquelle se trouve une machine sécurisée, elle aussi sécurisée par une clef. A-t-on vraiment besoin d’une clef sur la machine si on en a déjà une pour entrer dans la pièce ? Ça dépends des cas, souvent non.

On peut faire le parallèle avec Symfony, le handler est la machine, et le contrôleur la pièce sécurisée. Libre à vous d’ajouter des autorisations si vous en avez le besoin.

Voir l’étude de cas
Lire l’article
Voir le témoignage
Fermer