Listeners and Subscribers
Two ways to attach code to an event: a listener configured from outside, or a subscriber that declares its own events.
#[AsEventListener(event: 'kernel.response', method: 'onKernelResponse', priority: 0)]An event by itself does nothing. Something has to say "call this method when that event is dispatched". Symfony offers two forms, and they end up in the same place: one ordered list of callables per event.
Listener
A listener is any class with a method to call. It does not implement an interface and holds no mention of the event name. The wiring lives outside it: either the kernel.event_listener tag in configuration, or the #[AsEventListener] attribute, which is the same tag written on the class. When the event is not named, Symfony uses __invoke() and reads the event from the type declaration of its argument. When the event is named but the method is not, Symfony derives on plus the PascalCased event name and falls back to __invoke() only if the class has no such method.
Subscriber
A subscriber implements EventSubscriberInterface and answers one static question: which events, which methods, in which order. Nothing outside the class needs configuring, because Symfony registers any class implementing that interface automatically.
Which to prefer
Prefer a subscriber for your own application code: everything about it is readable in the file. Prefer a listener when the wiring belongs to configuration — a class reused for several events, or one registered only in some environments.
Priority
Each registration carries an integer priority, default 0. Higher runs earlier, negative runs after the defaults. If order matters, set it explicitly rather than relying on equal priorities.
Example
<?php
// src/EventSubscriber/RequestIdSubscriber.php
declare(strict_types=1);
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
final class RequestIdSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
// Longhand: a list of [method, priority] pairs.
// 1024 is early, before routing.
KernelEvents::REQUEST => [['onKernelRequest', 1024]],
// Shorthand for [['onKernelResponse', 0]].
// The two forms are the same thing.
KernelEvents::RESPONSE => 'onKernelResponse',
];
}
public function onKernelRequest(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$request = $event->getRequest();
$id = $request->headers->get('X-Request-Id') ?? bin2hex(random_bytes(8));
// attributes is the request's own scratch space.
$request->attributes->set('app_request_id', $id);
}
public function onKernelResponse(ResponseEvent $event): void
{
$id = $event->getRequest()->attributes->get('app_request_id');
if (is_string($id)) {
$event->getResponse()->headers->set('X-Request-Id', $id);
}
}
}When to use it
- Add a header or a request id to every response without editing a single controller.
- Reject requests to a group of routes before the controller runs.
- Find out which listeners already answer an event, and at what priority, before adding your own.
- Register a class on an event only in the dev environment.
More examples
The same job as a listener
One event, one method, no interface. Every response to a browser request now carries those two headers; the status code and body are untouched. Note what the class does not say: the event name appears nowhere in the body, only in the attribute. That is the trade. A listener is shorter, and a reader has to look at the attribute or the configuration to learn when it runs.
<?php
// src/EventListener/SecurityHeadersListener.php
declare(strict_types=1);
namespace App\EventListener;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
// Event inferred from the ResponseEvent type declaration.
// Negative priority: run after the framework's own response listeners.
#[AsEventListener(priority: -100)]
final class SecurityHeadersListener
{
public function __invoke(ResponseEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$headers = $event->getResponse()->headers;
$headers->set('X-Content-Type-Options', 'nosniff');
$headers->set('Referrer-Policy', 'same-origin');
}
}The attribute is a tag, written out
Shows that the attribute and the tag are two spellings of one registration, which matters when you read an older project that has no attributes in it. Use the YAML form when the wiring is a deployment decision rather than a property of the class: a listener registered in only one environment, or one whose priority is tuned per installation. Run php bin/console debug:event-dispatcher to see the resulting ordered list, whichever form produced it.
# config/services.yaml
services:
# Longhand. This is exactly what #[AsEventListener(priority: -100)]
# produces on App\EventListener\SecurityHeadersListener.
App\EventListener\SecurityHeadersListener:
tags:
- name: kernel.event_listener
event: kernel.response
method: __invoke
priority: -100
# Shortest form of the tag. Symfony then reads the event from the
# method's type declaration and finds __invoke() by itself.
App\EventListener\OtherListener:
tags: [kernel.event_listener]One class, several events
The attribute is repeatable, so one class can serve several events with a different method each. Here the event is named explicitly because the methods are not called onKernelController and onKernelTerminate. This is where a subscriber would be the better choice: two registrations and two methods is the point at which the ordering is easier to read inside getSubscribedEvents() than spread across attributes. Keep the listener form when the methods are genuinely independent.
<?php
// src/EventListener/RequestTraceListener.php
declare(strict_types=1);
namespace App\EventListener;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Psr\Log\LoggerInterface;
#[AsEventListener(event: KernelEvents::CONTROLLER, method: 'onController')]
#[AsEventListener(event: KernelEvents::TERMINATE, method: 'onTerminate')]
final class RequestTraceListener
{
// LoggerInterface is injected, not fetched from anywhere.
public function __construct(
private readonly LoggerInterface $logger,
) {
}
public function onController(ControllerEvent $event): void
{
$this->logger->debug('Controller resolved', [
'path' => $event->getRequest()->getPathInfo(),
]);
}
public function onTerminate(TerminateEvent $event): void
{
$this->logger->debug('Request finished', [
'status' => $event->getResponse()->getStatusCode(),
]);
}
}
Discussion