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. Quelques chiffres • 8 développeurs !""""""" • ini0alement, 6 mois

    • 59 769 lignes de PHP écrites (sans les tests, Behat) • 584 classes • 1 611 fichiers
  2. 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
  3. “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
  4. 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)
  5. /** * @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 { /* ... */ } }
  6. 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(); } } }
  7. Un exemple - notre cas Un explorateur de fichier en

    Javascript : Toutes les requêtes sont envoyées en AJAX à une route du backend.
  8. 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
  9. 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.
  10. 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; } }
  11. 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 } }
  12. 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]); }
  13. 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.
  14. 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 !
  15. 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
  16. 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 !