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 Slide

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

    View Slide

  3. Le projet : une suite du style ERP

    View Slide

  4. View Slide

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

    View Slide

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

    View Slide

  7. View Slide

  8. View Slide

  9. Les stratégies mises en place

    View Slide

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

    View Slide

  11. Bounded contexts
    Source : Mar+n Fowler

    View Slide

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

  13. 2/ Un modèle riche

    View Slide

  14. Modèle anémique

    View Slide

  15. Fat managers

    View Slide

  16. Logique mé+er

    View Slide

  17. 3/ Documenter le modèle

    View Slide

  18. Ubiquitous language

    View Slide

  19. View Slide

  20. Et des pa)erns pour nous aider

    View Slide

  21. “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 Slide

  22. 4/ Évènements

    View Slide

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

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

    View Slide

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

    View Slide

  26. /**
    * @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 Slide

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

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

    View Slide

  29. 5/ Command bus

    View Slide

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

    View Slide

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

    View Slide

  32. View Slide

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

  34. View Slide

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

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

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

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

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

    View Slide

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

  41. View Slide

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

  43. Conclusion

    View Slide

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

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

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

    View Slide

  47. One more thing...

    View Slide