Chargement...
Accueil /  Blog /  Symfony /  Comment désactiver certains listeners lors de certaines commandes

Comment désactiver certains listeners lors de certaines commandes

Publié le mardi 11 janvier 2022

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.

  1. c'est inutile, il suffirait de recharger le sitemap une seule fois, à la fin
  2. 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é

Suivez notre actualité en avant première. Pas plus d’une newsletter par mois.