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 full-size slide

  2. Romaric Drigon
    Software engineer @ netinfluence, Suisse

    View full-size slide

  3. Ce que l'on souhaite éviter

    View full-size 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 full-size slide

  5. Quelle serait une belle architecture ?

    View full-size 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 full-size slide

  7. 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 full-size slide

  8. 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 full-size slide

  9. C'est l'embouteillage !

    View full-size slide

  10. Notre objectif

    View full-size slide

  11. 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 full-size slide

  12. 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 full-size slide

  13. 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 full-size slide

  14. 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 full-size slide

  15. 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 full-size slide

  16. Cela évolue très bien !

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  19. 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 full-size slide

  20. 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 full-size slide

  21. 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 full-size slide

  22. 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 full-size slide

  23. romaric@netinfluence.ch
    http://netinfluence.ch

    View full-size slide