If you've ever needed to run an event listener before one provided by Symfony or a third-party bundle, you had to find its priority, pick a slightly higher number and hope that priority doesn't change in a future update.
In Symfony 8.2, you can solve this in a better way: the new before and
after options let you name the event listeners or tagged services
that yours should run before or after.
Ordering Event Listeners
Suppose your listener sets the _locale request attribute. It needs to run
before Symfony's LocaleListener reads that attribute to set the request's
locale. You can now declare that directly on your listener:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// src/EventListener/RequestLocaleListener.php
namespace App\EventListener;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\EventListener\LocaleListener;
#[AsEventListener(before: LocaleListener::class.'::onKernelRequest')]
final class RequestLocaleListener
{
public function __invoke(RequestEvent $event): void
{
// ...
}
}
Both options accept a service ID or class name, or an array of them if you need
to refer to several listeners. The ::onKernelRequest suffix selects a
specific method. This matters here because LocaleListener has two methods
that listen to kernel.request. Without the suffix, your listener would run
before both of them.
Event subscribers registered as services can use these options too. Symfony 8.2
also lets you use named keys such as method and priority in
getSubscribedEvents(), so you can write:
1 2 3 4 5 6 7 8 9
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => [
'method' => 'onKernelRequest',
'before' => LocaleListener::class.'::onKernelRequest',
],
];
}
A few things to keep in mind:
- Ordering is resolved when compiling the container, with no extra runtime work;
- Leave out the priority to let Symfony choose it. If you set one, constraints can only reorder listeners within that priority;
- References to listeners missing from the event are ignored (this way, you can support e.g. optional bundles);
- Conflicting priorities, circular constraints and invalid methods cause compilation errors.
Ordering Tagged Services
The same options are available when ordering tagged services. Suppose an import
bundle provides CSV and XLSX handlers with the same priority, and you want to
add your own TSV handler between them. You can express both requirements with
AsTaggedItem or in the service's tag configuration:
1 2 3 4 5 6 7 8 9 10 11 12
// src/Handler/TsvHandler.php
namespace App\Handler;
use Acme\ImportBundle\Handler\CsvHandler;
use Acme\ImportBundle\Handler\XlsxHandler;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
#[AsTaggedItem(after: CsvHandler::class, before: XlsxHandler::class)]
class TsvHandler
{
// ...
}
In the PHP example, the handler must already have the tag used by the import
bundle, for example through autoconfiguration. AsTaggedItem configures its
order; it doesn't add the tag itself.
Ordering Decorators
Service decorators had the same problem. When several decorators wrap a service, their priorities define which one is closer to the original service. To put yours in the right place, you had to check the priorities of the other decorators.
Symfony 8.2 adds two options for that: within lists the decorators that
wrap yours, and around lists the decorators that yours wraps. Suppose you
sign every outgoing HTTP request and want each retry to be signed again. Your
decorator must be inside Symfony's RetryableHttpClient:
1 2 3 4 5 6 7 8 9 10 11 12
// src/HttpClient/SigningHttpClient.php
namespace App\HttpClient;
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\HttpClient\RetryableHttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
#[AsDecorator('http_client', within: RetryableHttpClient::class)]
class SigningHttpClient implements HttpClientInterface
{
// ...
}
These options work the same way as before and after: they accept service
IDs or class names (or a list of them), names that don't match any other
decorator of the same service are ignored, and if you leave out the priority,
Symfony chooses it for you. They also work with #[AsTagDecorator] when
decorating all services with a given tag.
Ordering Doctrine Listeners and Messenger Handlers
Doctrine event listeners and Messenger handlers are ordered by priority too, and
they get the same before and after options. The doctrine.event_listener
tag uses them to order the listeners of an event on a connection:
1 2 3 4 5 6 7
# config/services.yaml
services:
App\EventListener\SearchIndexer:
tags:
- name: 'doctrine.event_listener'
event: 'postPersist'
after: 'App\EventListener\AuditLogger'
Support for these options in the #[AsDoctrineListener] attribute is
proposed in DoctrineBundle.
The #[AsMessageHandler] attribute and the messenger.message_handler tag
use them to order the handlers of a message on a bus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// src/MessageHandler/SendWelcomeEmailHandler.php
namespace App\MessageHandler;
use App\Message\UserRegistered;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler(after: AuditLogHandler::class)]
final class SendWelcomeEmailHandler
{
public function __invoke(UserRegistered $message): void
{
// ...
}
}
Constraints only apply between handlers of the same message class. Handlers of a
parent class, of an interface, or registered for * can't be referenced.
Ordering Firewall Listeners
Bundles that add firewall listeners, for example to handle two-factor
authentication, also had to pick priorities relative to Symfony's own listeners.
The new #[FirewallListenerOrder] attribute lets them name those listeners
instead:
1 2 3 4 5 6 7 8 9 10
use Symfony\Component\Security\Http\Attribute\FirewallListenerOrder;
use Symfony\Component\Security\Http\Firewall\AbstractListener;
use Symfony\Component\Security\Http\Firewall\AccessListener;
use Symfony\Component\Security\Http\Firewall\ContextListener;
#[FirewallListenerOrder(after: ContextListener::class, before: AccessListener::class)]
final class TwoFactorListener extends AbstractListener
{
// ...
}
Firewall listeners always have a priority, so these constraints only reorder the listeners that share it. A constraint that would need a different priority makes compiling the container fail. This way, a bundle can't move Symfony's own security listeners by mistake.
Using Them as Safety Checks
You can also keep an explicit priority and add before or after as a
check. Symfony now does this for its own listeners and argument value resolvers
whose priority was chosen relative to another one. For example, the resolvers of
Request, Session and #[CurrentUser] arguments declare that they run
before Doctrine's EntityValueResolver. Their priorities didn't change, but if
one of them is ever lowered below the Doctrine one, compiling the container fails
instead of silently changing which resolver handles those arguments.
For more examples, see the docs on ordering event listeners, ordering tagged services and ordering decorators.
These three new features can be very useful. Thank you so much for your contributions. You're doing a wonderful job.
Massive thanks for these three new features! They’re super handy, and you guys are absolutely rocking it.