Éviter un contrôleur de 1000 lignes: vite, un bus de commande!
Le but de cette présentation est de parler les tactiques utilisées afin de structurer un projet construit sur un métier complexe, varié, et appelé à grossir dans le temps. Et d'éviter de tout mettre dans le contrôleur ou un manager.
Notre objectif un code simple à comprendre avoir des objets simples à tester unitairement ne pas se répéter utiliser une architecture connue, plutôt que d'en inventer une : plus simple à expliquer, à communiquer autour bénéficier de retours d'expérience
La solution : un bus de commande Une commande... "...an object is used to encapsulate all information needed to perform an action..." ...et un bus "...a communication system that transfers data between components..."
Un cas d'utilisation Un explorateur de fichier en JS. Sur chaque fichier, des actions sont possibles: renommer, supprimer... Toutes les requêtes sont envoyées en AJAX à une route d'un backend Symfony.
Ce que le backend doit gérer l'utilisateur a-t-il le droit de faire cette action sur ce fichier? authentification permissions valider que la requête est valide l'exécuter logguer (audit) mettre à jour les quotas de l'utilisateur ...
Mise en place Nous allons utiliser Tactician (https://tactician.thephpleague.com/) Développé par Ross Tuck. Un bundle est disponible. D'autres implémentations existent, comme SimpleBus, de Matthias Noback. Ou encore le nouveau composant Messenger de Symfony.
Une commande namespace AppBundle\Model\Command; use AppBundle\Entity\File; class RemoveCommand { private $file; public function __construct(File $file) { $this->file = $file; } public function getFile(): File { return $this->file; } }
Le handler en charge namespace AppBundle\Handler; use AppBundle\Entity\File; use AppBundle\Model\Command\RemoveCommand; class RemoveHandler { private $entityManager; // A injecter public function handle(RemoveCommand $removeCommand): string { $item = $removeCommand->getFile(); $this->entityManager->remove($file); $this->entityManager->flush(); return $file->getUuid(); // Un handler peut retourner une valeur } }
Déclaration du handler Déclaré en tant que services Symfony: Note: Les handlers peuvent maintenant être autowirés, même sans Symfony 3.3. app.remove_handler: class: AppBundle\Handler\RemoveHandler tags: - { name: tactician.handler, command: AppBundle\Model\Command\RemoveComman arguments: - '@doctrine.orm.default_entity_manager'
Envoi de la commande dans le bus Dans le contrôleur Symfony: Cool Romaric ... mais c'est quoi l'intérêt ? /** * @ParamConverter("file") */ public function apiAction(File $file) { $command = new RemoveCommand($file); $uuid = $this->get('tactician.commandbus')->handle($command); return new JsonResponse(['uuid' => $uuid]); }
Quelques exemples de middlewares Doctrine (https://tactician.thephpleague.com/plugins/doctrine/) : exécute le handlers au sein d'une seule transaction Logger (https://tactician.thephpleague.com/plugins/logger/) : loggue tout dans Monolog (-> audit !) Validation (fourni avec le bundle Symfony Tactician) Security, pour l'autorisation ...
Quelques autres avantages Test : les handlers et middlewares sont faciles à tester unitairement. Organisation du travail : Chaque fonctionnalité étant dans des classes séparées, le travail est plus simple à répartir entre développeurs. Il y a moins de merge conflicts. Extensions : On peut connecter le bus sur une message queue (RabbitMQ...), ou les exécuter plus tard (tâches planifiées...).
Quelques inconvénients Formation : Souvent le pattern n'est pas connu. Il faut l'expliquer à l'équipe. Le noter quelque part (README...). Construction des commandes : Il faut construire les objets dans le contrôleur, alors utiliser un service qui sera une Factory. Debug : Suivre ce qui se passe est plus difficile. Wrapper le CommandBus dans un qui appelle le profiler SF.