Éviter un contrôleur de 1000 lignes: vite, un bus de commande!

É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.

88a681988c6744a099be88084dedb545?s=128

Romaric Drigon

March 30, 2018
Tweet

Transcript

  1. Éviter un contrôleur de 1000 lignes: Vite, un bus de

    commande ! SymfonyLive Paris 2018
  2. Romaric Drigon Software engineer @ netinfluence, Suisse

  3. Ce que l'on souhaite éviter

  4. 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
  5. Quelle serait une belle architecture ?

  6. 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..."
  7. None
  8. 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.
  9. 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 ...
  10. C'est l'embouteillage !

  11. Notre objectif

  12. 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.
  13. Une commande <?php 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; } }
  14. Le handler en charge <?php 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 } }
  15. 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'
  16. 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]); }
  17. Cela évolue très bien !

  18. L'application grossit... Un mois plus tard, nous avions 25 commandes

    et handlers, pour un total de 3500 lignes de code.
  19. But wait, there's more ! Les middlewares

  20. 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 ...
  21. 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...).
  22. 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.
  23. <?php class DebugCommandBus extends CommandBus { private function createExecutionChain($middlewareList) {

    $lastCallable = function () { // the final callable is a no-op }; while ($middleware = array_pop($middlewareList)) { if (!$middleware instanceof Middleware) { throw InvalidMiddlewareException::forMiddleware($middleware); } $name = get_class($middleware); $lastCallable = function ($command) use ($middleware, $lastCallable, $ $this->stopwatch->start($name, 'tactician'); $this->logger->debug(sprintf('Calling %s middleware with %s comman $return = $middleware->execute($command, $lastCallable); $this->stopwatch->stop($name); return $return; };
  24. romaric@netinfluence.ch http://netinfluence.ch