Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

Romaric Drigon

March 30, 2018
Tweet

More Decks by Romaric Drigon

Other Decks in Programming

Transcript

  1. Éviter un contrôleur de 1000 lignes:
    Vite, un bus
    de commande !
    SymfonyLive Paris 2018

    View Slide

  2. Romaric Drigon
    Software engineer @ netinfluence, Suisse

    View Slide

  3. Ce que l'on souhaite éviter

    View Slide

  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

    View Slide

  5. Quelle serait une belle architecture ?

    View Slide

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

    View Slide

  7. View Slide

  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.

    View Slide

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

    View Slide

  10. C'est l'embouteillage !

    View Slide

  11. Notre objectif

    View Slide

  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.

    View Slide

  13. 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;
    }
    }

    View Slide

  14. 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
    }
    }

    View Slide

  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'

    View Slide

  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]);
    }

    View Slide

  17. Cela évolue très bien !

    View Slide

  18. L'application grossit...
    Un mois plus tard, nous avions 25 commandes et handlers, pour
    un total de 3500 lignes de code.

    View Slide

  19. But wait, there's more !
    Les middlewares

    View Slide

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

    View Slide

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

    View Slide

  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.

    View Slide

  23. 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;
    };

    View Slide

  24. [email protected]fluence.ch
    http://netinfluence.ch

    View Slide