How to Convert Listeners to Subscribers and Reduce your Configs

I wrote Don't Ever use Symfony Listeners 2 months ago (if you missed it, be sure to read it to better understand this 2nd part). It got many constructive comments, mostly focused on particular standalone sentences without context.

To my surprise, none of the comments shown that listener beats subscriber.
But what can you do, if you'd like to try subscribers, but currently have over 100 listeners in your application?

2 Ways to do One Thing? = WTF! WHY TF?

Just a reminder, how hurtful is to teach people 2 very similar ways to do one thing.

Google shows that people are confused since 2012, wow!

And this is not related only to big patterns as subscriber or listener. We do such decisions every day - while we code a new feature when we add a new package to composer.json when we integrate 4th API to verify payments.

Next time you'll be standing before 2 options, remember the least common denominator and make your code more durable in time.

Why Should we Re-Think Listeners in our Code?

If the readable and clear code is not good enough reason for you and you still think you should stick with listeners at all cost, maybe the following steps will convince you.

Symfony 3.3 introduced PSR-4 Autodiscovery of services. In short, it means we don't have register services manually, if they respect PSR-4 (class name ~= file location):

 services:
-    App\Controller\CoffeeController: ~
-    App\Controller\WifiController: ~
-    App\Controller\PlaneController: ~
-    # thousands more...

+    App\Controller\:
+        resource: '../src/Controller'

How to Migrate Listeners to Subscribers?

It doesn't apply only to controllers, but to all services, like Event Subscribers:

 services:
     _defaults:
         # this helps load event subscribers to EventDistpatcher
         autoconfigure: true

-    App\EventSubscriber\CoffeeEventSubscriber: ~
-    App\EventSubscriber\WifiEventSubscriber: ~
-    App\EventSubscriber\PlaneEventSubscriber: ~
-    # thousands more...

+    App\EventSubscriber\:
+        resource: '../src/EventSubscriber'

Next time we create an event subscriber class, we don't have to code in config anymore

We've reduced cognitive load → code is easier to work with → hiring is faster → we can focus on business and feature value.

How can we Reduce configs with Listeners?

services:
    App\EventListener\WifiEventListener:
        tags:
            - { name: kernel.event_listener, event: kernel.exception }
            - { name: kernel.event_listener, event: kernel.view }

Well, what about...

services:
    App\EventListener\:
        resource: '../src/EventListener'

Hm, how can be the listener called when it has now an information event?

That's one of legacy code smells of tags.

We can't reduce configs. We have to grow about configs together with code till the end of times


If you're paid or motivated by productivity like me and not by produced lines of code or wasted time with no output, you care about this.

Automated Instant Migration

It's very nice use case for pattern refactoring, from A - Listener to B - Event Subscriber.

1. Define Patterns

A. Listener

B. Event Subscriber

2. Pattern Change In Code?

<?php

class SomeListener
{
     public function methodToBeCalled()
     {
     }
}
# in config.yaml
services:
    SomeListener:
        tags:
            - { name: kernel.event_listener, event: 'some_event', method: 'methodToBeCalled' }

<?php
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class SomeEventSubscriber implements EventSubscriberInterface
{
     /**
      * @return mixed[]
      */
     public static function getSubscribedEvents(): array
     {
         return ['some_event' => 'methodToBeCalled'];
     }

     public function methodToBeCalled()
     {
     }
}

Without any config.

3. Instant Upgrade with Rector

The latest Rector v0.5.8 is shipped with rule exactly for this kind of migration.

Just register the rule in your rector.yaml config to start migration:

# rector.yaml
services:
    Rector\SymfonyCodeQuality\Rector\Class_\EventListenerToEventSubscriberRector: ~

# optional, when something fails
parameters:
    kernel_class: "App\Kernel" # use explicit Kernel, if not discovered by Rector
    kernel_environment: "test" # use explicit environment, if not found by Rector

Run it:

vendor/bin/rector process app src

And now all the listeners were migrated to event subscribers

4. Update Configs

In the end, we have to remove all listeners + metadata from configs and add single autodiscovery for our EventSubscribers:

services:
-    App\EventListener\WifiEventListener:
-        tags:
-            - { name: kernel.event_listener, event: kernel.exception }
-            - { name: kernel.event_listener, event: kernel.view }

+    _defaults:
+        autoconfigure: true
+
+    App\EventSubscriber\:
+        resource: '../src/EventSubscriber'

That's it!


Happy coding!


Found a typo? Fix it to join team of 72 people that improve content here

❤️️ Do you like what I write about? Or do you hate it but enjoy discussion? 😠
You can support my writing by throwing a couple bucks at my Patreon