Another anti-pattern that deserves more attention than it has. I often see this in Symfony projects I consult and when I ask the dev why did he or she choose listener over subscriber, they don't really know - "it was in the Symfony documentation, you can read it there".
Not good enough. So why you should never use a listener?
When we look into Symfony EventDispatcher documentation, this is the first YAML code we see:
# config/services.yaml
services:
App\EventListener\ExceptionListener:
tags:
- { name: kernel.event_listener, event: kernel.exception }
If I'd be in the process of learning Symfony, this would be my thoughts:
Btw, there is not even "Subscriber" in the main headline!
That's how you write a manipulative text if you wanted people to never use subscribers :).
name
& event
?_autoconfigure: true
help you here?kernel_exception
or kernel.error
? Well, neitherAll these problems will shoot you or your colleague in the back in the future. You've just opened doors for 6 more possible bugs and problems to come to your project #carpeyolodiem.
Most of these problems are a result of config programming - that just sucks.
Listeners have only one valid use case - it's a 3rd party code located in your /vendor
and someone else wants you to use it with event of your choice in config, e.g.:
# config/services.yaml
services:
Vendor\ThirdPartyProject\Listener\UseMeListener:
tags:
- { name: kernel.event_listener, event: kernel.exception }
- { name: kernel.event_listener, event: kernel.view }
If it would be a subscriber, it would be very similar to this:
<?php
namespace App;
use Vendor\ThirdPartyProject\Listener\UseMeListener;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class YourListener extends UseMeListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
KernelEvents::EXCEPTION => ['onKernelException'],
KernelEvents::VIEW => ['onKernelView'],
];
}
}
What's wrong with this code? First, UseMeListener
should be final
, so you cannot break SOLID like this.
Let's take part by part.
$myEvents = [
KernelEvents::EXCEPTION => ['onKernelException'],
KernelEvents::VIEW => ['onKernelView'],
];
Using KernelEvents::EXCEPTION
constant is a big win! Instead of some string a config that we cannot analyze nor refactor, we have a constant. If you create a typo like KernelEvents::EXCEPTON
, you'll know. If you make a typo in a config? Good luck!
How is 'onKernelView'
string made? I have no idea. It's a convention name, that is somehow resolved from tag name in the config to a protected/public? local method that is called. We don't need that magic, right Mr. Potter?
If someone has created a listener that you can re-use, it's an anti-pattern already.
People actually do that, the StackOverflow has dozens of questions like "how to call command in a controller" or "how to controller in a command".
Command, Controller, EventSubscriber, Listener - they all should be only delegating code to a model layer service. If you need to mutually call or inherit one in another, you're creating a code smell. That's a sign that you should decouple common logic to a service and pass it via constructor to both.
So instead of giving people the option to use your code wrong way, give them a service, they can call in e.g. the EventSubscriber.
EventSubscriber has own interface, that guides you:
<?php
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class MyCustomEventSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
}
}
There is still a bit of magic... what should be in getSubscribedEvents()
method? Honestly, I have no idea. I don't want to remember what's written in code. So I'll use PHPStorm:
The trade-off worth the change
If you use KernelEvents::VIEW
constants within PHP code, you make the code also easier to debug.
Where is KernelEvents::VIEW
event actually dispatched? Just search KernelEvents::VIEW
(or better dispatch(KernelEvents::VIEW)
) in /vendor
and PHPStorm will show you the exact line. If you'd look for a string, it will lead to a false source of KernelEvents
(just a reference list of all Kernel events).
Also, when the event name is changed in a constant to view_event
, you don't mind. If you have view
in the config, good luck!
This makes using constants so fun. My rule of thumb is:
When the same string is used at 2 different classes,
it's worth creating a constant to make it typo-proof.
Imagine you have some code like:
# in class A
$configuration->setOption('resource');
# in class B
$input->getOption('resource');
Now you need to get this resource somewhere else. Was it "source", "sources", "resource" or "directory"? You don't care, constant autocomplete in PHPStorm tells you:
Don't remember what you don't need to.
Happy coding!
Do you learn from my contents or use open-source packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!