Search code examples
symfonysymfony4event-listener

Changing priority of a subscriber coming from a vendor's bundle


To set some context, I'm working on a Symfony 4.4 API, which use a vendor named EkinoNewRelicBundle to communicate data to the New Relic API. This vendor uses a subscriber named RequestListener to subscribe on the kernel.request event of Symonfy to define data to send to the New Relic API.

I've got an issue in a specific case, when there is an authentication issue resulting in a 498, another subscriber from the SecurityBundle throw an exception, stopping the request processing. Unfortunately, a method of the EkinoNewRelicBundle subscriber, named setTransactionName, has a lower priority than the SecurityBundle subscriber, resulting in data not properly set by Ekino as the process is stop.

By editing by hand the vendor, I found that a priority 10 on the setTransactionName would be enough to be executed before the SecurityBundle.

I'm looking for a way to edit the priority of the RequestListener priority at runtime. So far, I've tried to :

  • override the configuration, but as the priority is defined in a public static method from a class and seems to be loaded directly in the EventDispatcher, therefore it's no configuration to override ;
  • use a compiler pass to manipulate the definition, but same as above, there is no definition to edit as it seems to be loaded directly in the EventDispatcher ;
  • extend the Ekino subscriber to edit the priority, disabling the listner from the Ekino's configuration, but this results in configuration issue of my newly define subscriber as it can't be autowired.

Isn't there an easy way to change a priority of a subscribed event in a vendor's subscriber?

The maintener did talk about this seven years ago, when the configuration was still accessible with the compiler pass in this EkinoNewRelicBundle issue.


Solution

  • As per the How to Override any Part of a Bundle Symfony Documentation.

    If you want to modify the services created by a bundle, you can use service decoration.

    As the Ekino\NewRelicBundle\Listener\RequestListener is a service that is registered in the configs and autoconfigured by Symfony, you can create a decorator and add a custom getSubscribedEvents() method to override the priorities.

    Create the Decorator

    // /src/Decorator/EkinoRequestListenerDecorator.php
    
    namespace App\Decorator;
    
    use Ekino\NewRelicBundle\Listener\KernelRequestEvent;
    use Ekino\NewRelicBundle\Listener\RequestListener;
    use Symfony\Component\EventDispatcher\EventSubscriberInterface;
    use Symfony\Component\HttpKernel\KernelEvents;
    
    class EkinoRequestListenerDecorator implements EventSubscriberInterface
    {
    
        /** 
         * @var RequestListener
         */
        private $decorated;
    
        public function __construct(RequestListener $decorated)
        {
            $this->decorated = $decorated;
        }
    
        public static function getSubscribedEvents(): array
        {
            return [
                KernelEvents::REQUEST => [
                    ['setApplicationName', 255],
                    ['setIgnoreTransaction', 31],
                    ['setTransactionName', 10],
                ],
            ];
        }
    
        public function setApplicationName(KernelRequestEvent $event): void
        {
            $this->decorated->setApplicationName($event);
        }
    
        public function setIgnoreTransaction(KernelRequestEvent $event): void
        {
            $this->decorated->setIgnoreTransaction($event);
        }
    
        public function setTransactionName(KernelRequestEvent $event): void
        {
            $this->decorated->setTransactionName($event);
        }
    }
    

    Configure the Decorator

    The decorates option tells the container that the App\Decorator\EkinoRequestListenerDecorator service replaces the Ekino\NewRelicBundle\Listener\RequestListener service.

    This configuration replaces Ekino\NewRelicBundle\Listener\RequestListener with a new one, but keeps a reference of the old one as App\Decorator\EkinoRequestListenerDecorator.inner

    # /config/services.yaml
    
    services:
        # default configuration for services in *this* file
        _defaults:
            autowire: true      # Automatically injects dependencies in your services.
            autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
    
        # ...
    
        App\Decorator\EkinoRequestListenerDecorator:
            decorates: Ekino\NewRelicBundle\Listener\RequestListener
            arguments: ['@App\Decorator\EkinoRequestListenerDecorator.inner']
    

    Debug the event dispatcher

    php bin/console debug:event-dispatcher kernel.request
    

    Resulting kernel.request Event Dispatcher

    Before

    Registered Listeners for "kernel.request" Event
    ===============================================
    
     ------- ------------------------------------------------------------------------------------------------------- ---------- 
      Order   Callable                                                                                                Priority  
     ------- ------------------------------------------------------------------------------------------------------- ---------- 
      #3      Ekino\NewRelicBundle\Listener\RequestListener::setApplicationName()                                     255       
      #8      Ekino\NewRelicBundle\Listener\RequestListener::setIgnoreTransaction()                                   31            
      #21     Ekino\NewRelicBundle\Listener\RequestListener::setTransactionName()                                     -10       
     ------- ------------------------------------------------------------------------------------------------------- ---------- 
    
    

    After

    Registered Listeners for "kernel.request" Event
    ===============================================
    
     ------- ------------------------------------------------------------------------------------------------------- ---------- 
      Order   Callable                                                                                                Priority  
     ------- ------------------------------------------------------------------------------------------------------- ----------     
      #3      App\Decorator\EkinoRequestListenerDecorator::setApplicationName()                                       255       
      #8      App\Decorator\EkinoRequestListenerDecorator::setIgnoreTransaction()                                     31
      #13     App\Decorator\EkinoRequestListenerDecorator::setTransactionName()                                       10        
     ------- ------------------------------------------------------------------------------------------------------- ---------- 
    

    Resulting Container Event Listeners

            $instance->addListener('kernel.request', [0 => function () {
                return ($this->privates['App\\Decorator\\EkinoRequestListenerDecorator'] ?? $this->getEkinoRequestListenerDecoratorService());
            }, 1 => 'setApplicationName'], 255);
            $instance->addListener('kernel.request', [0 => function () {
                return ($this->privates['App\\Decorator\\EkinoRequestListenerDecorator'] ?? $this->getEkinoRequestListenerDecoratorService());
            }, 1 => 'setIgnoreTransaction'], 31);
            $instance->addListener('kernel.request', [0 => function () {
                return ($this->privates['App\\Decorator\\EkinoRequestListenerDecorator'] ?? $this->getEkinoRequestListenerDecoratorService());
            }, 1 => 'setTransactionName'], 10);
    
    
    //...
    
        protected function getEkinoRequestListenerDecoratorService()
        {
            return $this->privates['App\\Decorator\\EkinoRequestListenerDecorator'] = new \App\Decorator\EkinoRequestListenerDecorator(new \Ekino\NewRelicBundle\Listener\RequestListener(($this->privates['Ekino\\NewRelicBundle\\NewRelic\\Config'] ?? $this->getConfigService()), ($this->privates['Ekino\\NewRelicBundle\\NewRelic\\BlackholeInteractor'] ?? ($this->privates['Ekino\\NewRelicBundle\\NewRelic\\BlackholeInteractor'] = new \Ekino\NewRelicBundle\NewRelic\BlackholeInteractor())), [], [], new \Ekino\NewRelicBundle\TransactionNamingStrategy\RouteNamingStrategy()));
        }