Symfony 8.2 includes lots of performance improvements, but we keep looking for new opportunities to make your applications faster.
For example, Symfony creates the event dispatcher at runtime for every request
by calling addListener() for each listener and wrapping it in a closure,
even for events that the request never dispatches. But Symfony already knows
your application's event listeners when it compiles the container, so this
work can be done ahead of time.
Compiled Event Dispatcher
In Symfony 8.2, the event_dispatcher service is a CompiledEventDispatcher.
It receives an array of listener service IDs and methods, in the order they run,
and a service locator to load them when needed. PHP's OPcache stores the array
in shared memory, so each request doesn't need its own copy.
In a benchmark with 75 listeners across 31 events, creating the dispatcher took 0.48 µs, down from 33.7 µs. The listener closures previously used about 97 KB of memory; the compiled dispatcher doesn't need them.
Dispatching events is as fast as before. Listener services are still created only when they're about to run. If an event is never dispatched, or a listener stops its propagation, the remaining listeners aren't instantiated.
Your existing listener and subscriber registrations benefit from this
automatically. The profiler and debug:event-dispatcher work as before.
Changing Listeners at Runtime Is Deprecated
Adding or removing a listener at runtime forces the compiled dispatcher to
fall back to a regular EventDispatcher, losing the performance benefit.
Symfony 8.2 therefore deprecates calling addListener(), addSubscriber(),
removeListener() or removeSubscriber() on the shared event_dispatcher
service. These calls also trigger deprecations in dev, where you can see
them in the profiler.
Register your listeners as services instead. For temporary listeners, such as
those needed while a console command runs, use the new ScopedEventDispatcher.
It wraps the shared dispatcher and holds the extra listeners without adding them
to the shared service. Only events dispatched through the wrapper reach those
listeners:
1 2 3 4 5 6 7 8 9
use Symfony\Component\EventDispatcher\ScopedEventDispatcher;
use Symfony\Component\Messenger\EventListener\StopWorkerOnMessageLimitListener;
use Symfony\Component\Messenger\Worker;
$dispatcher = new ScopedEventDispatcher($this->eventDispatcher);
$dispatcher->addSubscriber(new StopWorkerOnMessageLimitListener(10));
$worker = new Worker($receivers, $bus, $dispatcher);
$worker->run();
The wrapper calls both sets of listeners in priority order. Symfony uses it in the
messenger:consume and messenger:failed:retry commands.
Check your functional tests too. If a test adds a listener to the shared
event_dispatcher to check that an event was dispatched, register that
listener as a service in the test environment instead.
New Autowiring Type for the Event Dispatcher
Autowiring the shared dispatcher with
Symfony\Component\EventDispatcher\EventDispatcherInterface is also
deprecated, because that interface includes methods for changing listeners.
If your service only dispatches events, switch to the contracts interface:
1 2 3 4 5 6 7 8 9 10 11 12
- use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+ use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class OrderPlacer
{
public function __construct(
private EventDispatcherInterface $dispatcher,
) {
}
// ...
}
To inspect registered listeners, for example in a debugging tool, autowire
Symfony\Contracts\EventDispatcher\ListenerIntrospectionInterface. This new
interface provides getListeners(), getListenerPriority() and
hasListeners(). If you also need to dispatch events, combine both interfaces
with an intersection type:
1 2 3 4 5 6 7
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\EventDispatcher\ListenerIntrospectionInterface;
public function __construct(
private EventDispatcherInterface&ListenerIntrospectionInterface $dispatcher,
) {
}