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.

88a681988c6744a099be88084dedb545?s=128

Romaric Drigon

September 26, 2019
Tweet

Transcript

  1. Structurer une applica-on mé#er avec Symfony Romaric Drigon @ Apéro

    PHP, AFUP Poi5ers, 26/09/2019
  2. Romaric Drigon So-ware engineer @ ASIT VD !

  3. Le projet : une suite du style ERP

  4. None
  5. Quelques chiffres • 8 développeurs !""""""" • ini0alement, 6 mois

    • 59 769 lignes de PHP écrites (sans les tests, Behat) • 584 classes • 1 611 fichiers
  6. Ce que l'on veut éviter...

  7. None
  8. None
  9. Les stratégies mises en place

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

  11. Bounded contexts Source : Mar+n Fowler

  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
  13. 2/ Un modèle riche

  14. Modèle anémique

  15. Fat managers

  16. Logique mé+er

  17. 3/ Documenter le modèle

  18. Ubiquitous language

  19. None
  20. Et des pa)erns pour nous aider

  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
  22. 4/ Évènements

  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)
  24. Mon code après avoir u0lisé des évènements

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

  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 { /* ... */ } }
  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(); } } }
  28. Code complet sur h"ps:/ /romaricdrigon.github.io/2019/08/09/domain-events

  29. 5/ Command bus

  30. Un exemple - notre cas Un explorateur de fichier en

    Javascript : Toutes les requêtes sont envoyées en AJAX à une route du backend.
  31. Ce que l'on veut éviter (bis)...

  32. None
  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
  34. None
  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.
  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; } }
  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 } }
  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]); }
  39. Un mois plus tard, nous avions 25 commandes et handlers

    (~3 500 lignes de code)
  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.
  41. None
  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 !
  43. Conclusion

  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
  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 !
  46. Merci de votre a,en.on ! h"ps:/ /romaricdrigon.github.io/

  47. One more thing...