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

Structurer une application métier avec Symfony

Structurer une application métier avec Symfony

À huit développeurs nous nous sommes attelés au re-développement d'une application complexe, comparable à un mini ERP, avec Symfony.
Cette présentation revient sur les tactiques utilisées afin de structurer un projet construit sur un métier complexe, varié, et appelé à grossir dans le temps. Comment avons-nous fait pour garder compréhensibles 60 000 lignes de PHP écrites en quelques mois ? Je vais expliquer comment nous avons appliqué des techniques issues du Domain Driven Development, qui ont permis d'augmenter la productivité de l'équipe, et vous partager quelques autres astuces découvertes au passage.

Romaric Drigon

September 26, 2019
Tweet

More Decks by Romaric Drigon

Other Decks in Programming

Transcript

  1. Structurer une applica-on
    mé#er avec Symfony
    Romaric Drigon @ Apéro PHP, AFUP Poi5ers, 26/09/2019

    View full-size slide

  2. Romaric Drigon
    So-ware engineer @ ASIT VD
    !

    View full-size slide

  3. Le projet : une suite du style ERP

    View full-size slide

  4. Quelques chiffres
    • 8 développeurs
    !"""""""
    • ini0alement, 6 mois
    • 59 769 lignes de PHP écrites (sans les tests, Behat)
    • 584 classes
    • 1 611 fichiers

    View full-size slide

  5. Ce que l'on veut éviter...

    View full-size slide

  6. Les stratégies mises en place

    View full-size slide

  7. 1/ « Diviser pour mieux régner »

    View full-size slide

  8. Bounded contexts
    Source : Mar+n Fowler

    View full-size slide

  9. Symfony 2 et 3 : bundles
    Symfony 4 : sous-dossiers et sous-namespaces
    # config/packages/doctrine.yaml
    doctrine:
    orm:
    mappings:
    App\Admin:
    is_bundle: false
    type: annotation
    dir: '%kernel.project_dir%/src/Admin/Entity'
    prefix: 'App\Admin\Entity'
    alias: App\Admin
    App\Catalog:
    is_bundle: false
    type: annotation
    dir: '%kernel.project_dir%/src/Catalog/Entity'
    prefix: 'App\Catalog\Entity'
    alias: App\Catalog

    View full-size slide

  10. 2/ Un modèle riche

    View full-size slide

  11. Modèle anémique

    View full-size slide

  12. Fat managers

    View full-size slide

  13. Logique mé+er

    View full-size slide

  14. 3/ Documenter le modèle

    View full-size slide

  15. Ubiquitous language

    View full-size slide

  16. Et des pa)erns pour nous aider

    View full-size slide

  17. “A pa&ern is not a cookbook.
    It lets you start from a base of
    experience to develop your solu;on,
    and it gives you some language to
    talk about what you are doing.”
    — Eric Evans, Domain-Driven Design

    View full-size slide

  18. 4/ Évènements

    View full-size slide

  19. Les avantages des évènements
    • perme&ent de découpler
    • évitent les répé**ons
    • les listeners sont des classes rela3vement pe*tes et simples
    • plus facilement testables
    • intégrés à Symfony (EventDispatcher)

    View full-size slide

  20. Mon code après avoir u0lisé
    des évènements

    View full-size slide

  21. 4/ Évènements
    4'/ Domain events

    View full-size slide

  22. /**
    * @ORM\Entity
    * @ORM\HasLifecycleCallbacks()
    */
    class User
    {
    // On stocke les évènements dans l'entité
    protected $events = [];
    public function enable()
    {
    if (self::STATUS_ENABLED !== $status) {
    // L'évènement est émis par l'entité
    $this->events[] = new UserEnabled($this->id, $this->email);
    }
    $this->status = self::STATUS_ENABLED;
    }
    /**
    * Au besoin, utiliser les lifecycle callbacks (update, remove).
    * @ORM\PreRemove
    */
    public function onRemove()
    {
    $this->events[] = new UserRemoved($this->id, $this->name);
    }
    // Retourne et vide $events
    public function popEvents(): array { /* ... */ }
    }

    View full-size slide

  23. class DomainEventsCollector implements EventSubscriber
    {
    private $events = []; // Domain events mis en attente
    private $eventDispatcher; // Event Dispatcher Symfony à injecter
    // On déclare un EventSubscriber Doctrine - aussi à faire sur postUpdate, postRemove...
    public function getSubscribedEvents() { return [Events::postPersist]; }
    public function postPersist(LifecycleEventArgs $event)
    {
    if (!$event->getEntity() instanceof User) { return; }
    foreach ($event->getEntity()->popEvents() as $event) {
    $this->events[spl_object_hash($event)] = $event; // On indexe par hash de l'objet, pour dé-dupliquer
    }
    }
    // Méthode qui sera à appeler (depuis nos controllers, depuis un ResponseListener...)
    public function dispatchCollectedEvents(): void
    {
    $events = $this->events;
    $this->events = [];
    foreach ($events as $event) {
    $this->eventDispatcher->dispatch(get_class($event), $event); // Syntaxe pour SF <= 4.3
    }
    if ($this->events) { // Des listeners peuvent ré-emettre des évènements
    $this->dispatchCollectedEvents();
    }
    }
    }

    View full-size slide

  24. Code complet sur
    h"ps:/
    /romaricdrigon.github.io/2019/08/09/domain-events

    View full-size slide

  25. 5/ Command bus

    View full-size slide

  26. Un exemple - notre cas
    Un explorateur de fichier en Javascript :
    Toutes les requêtes sont envoyées en AJAX à une route du backend.

    View full-size slide

  27. Ce que l'on veut éviter (bis)...

    View full-size slide

  28. Un bus de commande
    "...an object is used to encapsulate all informa5on needed to
    perform an ac5on..."
    — une commande
    "...a communica+on system that transfers data between
    components..."
    — un bus

    View full-size slide

  29. Mise en place
    Nous avons u)lisé Tac)cian, par Ross Tuck.
    Un bundle Symfony est disponible.
    De nombreuses implémenta1ons existent,
    comme SimpleBus, de Ma9hias Noback,
    ou encore avec le composant Messenger de Symfony.

    View full-size slide

  30. Une commande
    namespace App\Model\Command;
    use App\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

  31. Le handler
    namespace App\Handler;
    use App\Entity\File;
    use App\Model\Command\RemoveCommand;
    class RemoveHandler
    {
    private $entityManager; // À 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

  32. Envoi de la commande dans le bus
    /**
    * @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

  33. Un mois plus tard, nous avions
    25 commandes et handlers
    (~3 500 lignes de code)

    View full-size slide

  34. Encore quelques détails...
    • l'u%lisateur a-t-il le droit de faire ce2e ac%on sur ce fichier ?
    • valider que la requête est valide
    • l'exécuter
    • logguer (audit)
    • me2re à jour les quotas de l'u%lisateur
    • etc.

    View full-size slide

  35. Quelques exemples de middlewares
    Pour Tac)cian :
    • Doctrine : exécute le handler au sein d'une seule transac5on
    • Logger : loggue tout dans Monolog
    • Valida5on (fourni avec le bundle Symfony)
    • Security, pour l'autorisa5on
    • et les vôtres !

    View full-size slide

  36. Bilan sur les stratégies
    • vocabulaire commun: indispensable !
    • sauf si vous travaillez seul dans une gro1e
    • en4tés riches : très très recommandé
    • les en4tés resteront très longtemps dans le projet
    • ne soyez pas l'esclave de Doctrine ou de vos formulaires

    View full-size slide

  37. Bilan sur les pa,erns
    • + code plus facile à comprendre
    • + moins de répé44ons
    • + productivité++
    • + organisa4on plus facile en équipe (moins de conflits)
    • + plus facile à tester
    • - besoin d'explica4ons au début
    • Aidez-moi à faire connaître ces pa4erns !

    View full-size slide

  38. Merci de votre a,en.on !
    h"ps:/
    /romaricdrigon.github.io/

    View full-size slide

  39. One more thing...

    View full-size slide