Une solution simple et élégante, utilisant l'injection de services tagués, pour vous donner la possibilité de désactiver certains listeners lors de l'exécution de certaines commandes.
A quoi ça sert ?
C'est souvent pour des problématiques de performance, lors de traitements en masse.
Un exemple ?
Votre site dispose d'un sitemap (généré, car plus rapide).
Votre sitemap est alimenté par des URLs en provenance d'une base de données.
Dès que vos données changent, vous devez re-générer ce sitemap.
Vous disposz d'un listener qui, dès qu'un changement est détecté, demande la re-génération.
Problème : lors du chargement des fixtures, la re-génération du sitemap est demandée plusieurs fois.
- c'est inutile, il suffirait de recharger le sitemap une seule fois, à la fin
- c'est lent, selon le volume de données, cela peut pénaliser la durée de chargement de vos fixtures
Une solution simple et élégante ?
Il s'agit d'utiliser les mécaniques de Symfony et de respecter les bonnes pratiques de développement.
Notamment le principe ouvert/fermé de SOLID : je souhaite que chaque listener puisse déclarer les commande sur lesquelles celui-ci ne veut pas être exécuté, plutôt que d'avoir une classe/configuration qui dispose de la connaissance de l'ensemble des règles.
La première étape est de déclarer une interface que l'on pourra poser sur chacun de ces listeners :
<?php namespace App\Listener; /** * Un listener qui implémente cette interface * s'autorise à être désactivé lors de certaines commandes. * * Le plus souvent, désactiver un listener se fait * pour améliorer les performances de certaines commandes. */ interface ListenerDisabledForCommand { /** * @return string[] */ public static function getCommandsWhereDisabled(): array; }
Ensuite, nous allons écrire un listener, qui s'abonnera à l'événement "une commande est sur le point de démarrer", et qui désactivera les listeners qui ne doivent pas être enregistrés sur la commande en question.
<?php namespace App\Listener; use Doctrine\Common\EventSubscriber; use Doctrine\ORM\EntityManagerInterface; use Generator; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * Ce listener permet de désactiver d'autres listeners lors de l'exécution * de certaines commandes. * * Pour utiliser cette fonctionnalité un listener doit implémenter * l'interface {@see ListenerDisabledForCommand}. */ final class DisableListenersForCommandListener implements EventSubscriberInterface { public function __construct( /** * @var iterable&ListenerDisabledForCommand[] */ private iterable $listenersToDisable, private EntityManagerInterface $doctrine, private EventDispatcherInterface $eventDispatcher, ) { } public static function getSubscribedEvents(): Generator { yield ConsoleCommandEvent::class => 'onConsoleCommand'; } public function onConsoleCommand(ConsoleCommandEvent $event): void { $command = $event->getCommand(); if ($command === null) { return; } foreach ($this->listenersToDisable as $listener) { if (!\in_array($command->getName(), $listener::getCommandsWhereDisabled(), true)) { continue; } $this->removeDoctrineSubscriber($listener); $this->removeSymfonySubscriber($listener); } } private function removeSymfonySubscriber(object $subscriber): void { if ($subscriber instanceof EventSubscriberInterface) { $this->eventDispatcher->removeSubscriber($subscriber); } } private function removeDoctrineSubscriber(object $subscriber): void { if (!$subscriber instanceof EventSubscriber) { return; } $this->doctrine->getEventManager()->removeEventSubscriber($subscriber); } }
Enfin, via quelques lignes de configuration, nous allons ajouter un tag à tous nos listeners qui implémentent notre interface.
Et injecter dans notre listener, tous les services qui ont été tagués avec ce tag.
services: _instanceof: App\Listener\ListenerDisabledForCommand: tags: ['app.listener_disabled_for_command'] App\Listener\DisableListenersForCommandListener: arguments: $listenersToDisable: !tagged_iterator { tag: app.listener_disabled_for_command }
Exemple
Reprenons notre exemple plus haut : nous avons un listener qui demande la régénération du sitemap dès que certaines entités sont créées/modifiées/supprimées et nous voulons que ce listener ne soit pas appelé lors du chargement des fixtures.
<?php namespace App\Sitemap; use App\Listener\ListenerDisabledForCommand; use Doctrine\Common\EventSubscriber; final class TriggerSitemapDumpListener implements EventSubscriber, ListenerDisabledForCommand { /** * Afin d'éviter de re-générer le sitemap à chaque flush lors du chargement des fixtures, * ce listener est désactivé sur la commande associée. */ public static function getCommandsWhereDisabled(): array { return ['doctrine:fixtures:load']; } }
note : le code de ce listener a volontairement été simplifié, car ce n'est pas vraiment le propos.
Vous pouvez cependant retrouver le code en entier dans le snippet associé