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.
For more examples, see the docs on ordering event listeners, ordering tagged services and ordering decorators.