Updated June 2026. Tested on Symfony 7.x and PHP 8.4. Part of the Techalyst Symfony series.

Events are how code reacts to something happening without the part that triggered it knowing anything about the reaction. Symfony dispatches events all through the request, and you can dispatch your own. It is the mechanism behind a lot of clean decoupling: when an order is placed, something sends a confirmation email and something else updates a report, and the code that placed the order knows about neither.

Listening to an event

The simplest way to react to an event is a listener: a method that runs when the event fires. You mark it with the #[AsEventListener] attribute and say which event it responds to:

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\ResponseEvent;

#[AsEventListener(event: ResponseEvent::class)]
class SecurityHeadersListener
{
    public function __invoke(ResponseEvent $event): void
    {
        $event->getResponse()->headers->set('X-Frame-Options', 'DENY');
    }
}

Symfony fires ResponseEvent for every response, and this listener adds a header to each one, without any controller knowing it exists.

Subscribers: one class, many events

A subscriber is the alternative when one class should handle several related events. Instead of an attribute per method, the class declares everything it listens to in getSubscribedEvents:

class OrderSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            OrderPlacedEvent::class => 'onOrderPlaced',
            OrderShippedEvent::class => 'onOrderShipped',
        ];
    }

    public function onOrderPlaced(OrderPlacedEvent $event): void { /* ... */ }
    public function onOrderShipped(OrderShippedEvent $event): void { /* ... */ }
}

The difference is about where the wiring lives. A listener's subscription sits in the attribute, external to the method. A subscriber keeps its list of events inside the class. Reach for a subscriber when one class owns a group of related event handling, and a listener for a single, standalone reaction.

Dispatching your own events

The real power is your own events. You dispatch one when something domain-meaningful happens, and any number of listeners can react, now or later, without you touching the dispatching code again:

class OrderPlacedEvent
{
    public function __construct(public readonly Order $order) {}
}

// where the order is placed
public function place(Order $order, EventDispatcherInterface $dispatcher): void
{
    // ...save the order
    $dispatcher->dispatch(new OrderPlacedEvent($order));
}

The event object just carries data. The code that places the order dispatches it and moves on. Sending the email, updating analytics, notifying the warehouse, each is a separate listener you can add, remove, or test in isolation, none of which the order code needs to know about.

Why this matters

This is decoupling in practice. Without events, the place method grows a tail of "and then email, and then log, and then notify" calls, all hard-wired. With events, place announces what happened and the reactions live elsewhere. New behaviour means a new listener, not an edit to the core logic. That is also exactly how Symfony itself stays extensible, the kernel events let you hook the request lifecycle without modifying the framework.

Wrapping up

The EventDispatcher lets code react to things happening without coupling to what caused them. Use a listener with #[AsEventListener] for a single reaction to an event, and a subscriber implementing EventSubscriberInterface when one class handles several related events. Dispatch your own events for domain moments like an order being placed, and let independent listeners handle the side effects. It keeps your core logic focused and your app open to new behaviour without rewrites.