Nicolas Grekas
Contributed by Nicolas Grekas in #66012 , #66022 , #66240 , #66236 , #66713 , #66714 , #66715 and #66718

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.

Published in #Living on the edge