So far in this course, almost everything you've written has been *directly* triggered by a request: a user visits a route, Drupal calls your controller. A user submits a form, Drupal calls your submit handler. But real applications need a looser kind of communication too — a way for one piece of code to say "something happened" without needing to know, or care, who (if anyone) is listening. That's exactly what Drupal's event system gives you, and it's what this lesson is about.
We'll start from the listening side: an event subscriber, the class that says "when this specific thing happens anywhere in the site, run my code." In the next lesson you'll see the other half — how something actually announces that the event happened in the first place.
What you'll learn in this lesson
- What an event subscriber is, and why it's a more decoupled alternative to calling code directly
- How to implement Symfony's
EventSubscriberInterface, the contract every subscriber must fulfill - How subscriber priority controls execution order when multiple subscribers listen to the same event
- How
stopPropagation()lets one subscriber prevent lower-priority ones from running
A quick mental model: think of it like a notice board
Imagine a notice board in an office. Someone pins up a note that says "Incident reported." Anyone who's interested in incidents can walk past that board, read the note, and act on it — call security, notify a manager, log it in a spreadsheet. The person pinning the note doesn't need to know who's going to read it, or how many people will react, or in what order. They just pin the note and walk away.
In Drupal's event system: pinning the note is dispatching an event. Walking past the board and reacting is what an event subscriber does. This lesson is about the "walking past and reacting" half.
The source file
Path (relative to the Examples module's root): modules/events_example/src/EventSubscriber/EventsExampleSubscriber.php
<?php
namespace Drupal\events_example\EventSubscriber;
use Drupal\events_example\Event\IncidentEvents;
use Drupal\events_example\Event\IncidentReportEvent;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Subscribe to IncidentEvents::NEW_REPORT events and react to new reports.
*
* In this example we subscribe to all IncidentEvents::NEW_REPORT events and
* point to two different methods to execute when the event is triggered. In
* each method we have some custom logic that determines if we want to react to
* the event by examining the event object, and the displaying a message to the
* user indicating whether or not that method reacted to the event.
*
* By convention, classes subscribing to an event live in the
* Drupal/{module_name}/EventSubscriber namespace.
*
* @ingroup events_example
*/
class EventsExampleSubscriber implements EventSubscriberInterface {
use StringTranslationTrait;
use MessengerTrait;
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
// Return an array of events that you want to subscribe to mapped to the
// method on this class that you would like called whenever the event is
// triggered. A single class can subscribe to any number of events. For
// organization purposes it's a good idea to create a new class for each
// unique task/concept rather than just creating a catch-all class for all
// event subscriptions.
//
// See EventSubscriberInterface::getSubscribedEvents() for an explanation
// of the array's format.
//
// The array key is the name of the event your want to subscribe to. Best
// practice is to use the constant that represents the event as defined by
// the code responsible for dispatching the event. This way, if, for
// example, the string name of an event changes your code will continue to
// work. You can get a list of event constants for all events triggered by
// core here:
// https://api.drupal.org/api/drupal/core%21core.api.php/group/events/8.2.x.
//
// Since any module can define and trigger new events there may be
// additional events available in your application. Look for classes with
// the special @Event docblock indicator to discover other events.
//
// For each event key define an array of arrays composed of the method names
// to call and optional priorities. The method name here refers to a method
// on this class to call whenever the event is triggered.
$events[IncidentEvents::NEW_REPORT][] = ['notifyMario'];
// Subscribers can optionally set a priority. If more than one subscriber is
// listening to an event when it is triggered they will be executed in order
// of priority. If no priority is set the default is 0.
$events[IncidentEvents::NEW_REPORT][] = ['notifyBatman', -100];
// We'll set an event listener with a very low priority to catch incident
// types not yet defined. In practice, this will be the 'cat' incident.
$events[IncidentEvents::NEW_REPORT][] = ['notifyDefault', -255];
return $events;
}
/**
* If this incident is about a missing princess notify Mario.
*
* @param \Drupal\events_example\Event\IncidentReportEvent $event
* The event object containing the incident report.
*/
public function notifyMario(IncidentReportEvent $event) {
if ($event->getType() == 'stolen_princess') {
$this->messenger()->addStatus($this->t('Mario has been alerted. Thank you. This message was set by an event subscriber. See @method()', ['@method' => __METHOD__]));
$event->stopPropagation();
}
}
/**
* Let Batman know about any events involving the Joker.
*
* @param \Drupal\events_example\Event\IncidentReportEvent $event
* The event object containing the incident report.
*/
public function notifyBatman(IncidentReportEvent $event) {
if ($event->getType() == 'joker') {
$this->messenger()->addStatus($this->t('Batman has been alerted. Thank you. This message was set by an event subscriber. See @method()', ['@method' => __METHOD__]));
$event->stopPropagation();
}
}
/**
* Handle incidents not handled by the other handlers.
*
* @param \Drupal\events_example\Event\IncidentReportEvent $event
* The event object containing the incident report.
*/
public function notifyDefault(IncidentReportEvent $event) {
$this->messenger()->addStatus($this->t('Thank you for reporting this incident. This message was set by an event subscriber. See @method()', ['@method' => __METHOD__]));
$event->stopPropagation();
}
}
How it works
Namespace and file location
This class lives in Drupal\events_example\EventSubscriber, which maps to the physical folder src/EventSubscriber/ inside the module. This isn't arbitrary — it's a PSR-4 convention Drupal relies on so it can auto-discover subscriber classes registered as services. Any subscriber class you write should follow the exact same pattern: src/EventSubscriber/YourSubscriberClass.php.
Implementing EventSubscriberInterface
class EventsExampleSubscriber implements EventSubscriberInterface {
EventSubscriberInterface comes from Symfony\Component\EventDispatcher — Drupal builds its own event system directly on top of Symfony's. The interface requires exactly one method, getSubscribedEvents(). Implementing it is the whole contract: it's how you tell Drupal "I want to be notified about things."
Two helper traits
use StringTranslationTrait;
use MessengerTrait;
PHP traits let you pull in reusable methods without inheritance. StringTranslationTrait gives you $this->t() for translatable strings — the same helper you've been using in forms and controllers. MessengerTrait gives you $this->messenger(), which returns Drupal's messenger service for showing on-screen status messages. Using these traits keeps a subscriber class short without you having to manually inject and store two extra services in a constructor.
getSubscribedEvents(): declaring what you're listening for
public static function getSubscribedEvents(): array {
$events[IncidentEvents::NEW_REPORT][] = ['notifyMario'];
$events[IncidentEvents::NEW_REPORT][] = ['notifyBatman', -100];
$events[IncidentEvents::NEW_REPORT][] = ['notifyDefault', -255];
return $events;
}
This is the one required method, and it's static because Drupal calls it before the class is even instantiated — it just needs to know what you're interested in listening to. The return value is an associative array:
- Array key — the event name you're listening for, ideally a class constant like
IncidentEvents::NEW_REPORTrather than a raw string (you'll see why in the next lesson). - Array value — a list of listener definitions. Each one names a method on this class to call, with an optional priority.
Priority: who runs first
When more than one subscriber method listens to the same event, priority decides the order. Higher numbers run first; lower (more negative) numbers run last. The default, when you don't specify one, is 0.
| Subscriber method | Priority | Runs |
|---|---|---|
notifyMario | 0 (default) | First |
notifyBatman | -100 | Second |
notifyDefault | -255 | Last (fallback) |
That ordering is deliberate: notifyDefault is a catch-all that should only ever fire when nothing more specific already handled the event.
Reacting conditionally, and stopping the chain
public function notifyMario(IncidentReportEvent $event) {
if ($event->getType() == 'stolen_princess') {
$this->messenger()->addStatus(...);
$event->stopPropagation();
}
}
Every subscriber method receives the event object as its only argument — here typed as IncidentReportEvent, which you'll meet properly in a later lesson. Notice the method does nothing at all unless the incident type matches 'stolen_princess' — subscribers are completely free to ignore events that don't concern them. When it does match, it shows a status message and calls $event->stopPropagation(). That call is important: it tells the dispatcher "don't bother calling any more, lower-priority subscribers for this particular dispatch." Because notifyMario runs first (priority 0), a successful match here means notifyBatman and notifyDefault never run at all.
notifyBatman() is structurally identical, just checking for 'joker' instead, and running second. notifyDefault() has no condition at all — it always reacts, which is exactly what you want from a last-resort fallback with the lowest priority in the group.
stopPropagation() only affects subscribers for that specific dispatch of that specific event — it doesn't disable the subscriber for future events, and it doesn't affect subscribers listening to a completely different event name.The __METHOD__ trick
Every message in this class includes '@method' => __METHOD__. __METHOD__ is a PHP magic constant that expands, at runtime, to the fully-qualified class and method name — e.g. Drupal\events_example\EventSubscriber\EventsExampleSubscriber::notifyMario. The Examples module uses this trick constantly so that, when you're testing a page, the on-screen message tells you exactly which piece of code produced it. It's a genuinely useful habit to borrow for your own debugging messages while developing.
One more piece: registering the service
A subscriber class on its own isn't enough — Drupal has no way to find it unless it's registered as a tagged service in the module's *.services.yml file:
services:
events_example.subscriber:
class: Drupal\events_example\EventSubscriber\EventsExampleSubscriber
tags:
- { name: event_subscriber }
That { name: event_subscriber } tag is what actually gets this class discovered by Drupal's event dispatcher. We'll look at exactly how that file works — and how to write your own — in the third lesson of this topic.
See it for yourself
Visit /examples/events-example on your DDEV site, pick an incident type, and submit the form. Whichever message comes back — Mario, Batman, or the generic thank-you — was produced by exactly one of the three methods you just read, chosen entirely by the incident type and the priority order.
Quick check: if you reported a "Cat stuck in tree" incident, which method fires — and why don't
notifyMarioornotifyBatmanrun first and swallow it? (Because neither of theirifconditions matches'cat', so neither callsstopPropagation(), and execution falls all the way through to the lowest-priority fallback,notifyDefault.)
Key takeaways
- An event subscriber class must implement
EventSubscriberInterfaceand definegetSubscribedEvents()as astaticmethod mapping event names to handler methods on the same class. - A single subscriber class can listen to multiple events, and register multiple handler methods for the same event, each as its own entry in the returned array.
- Priority is an integer: higher numbers run first, lower (negative) numbers run last. Reserve a very low priority (like
-255) for fallback handlers. - Calling
$event->stopPropagation()inside a handler prevents all remaining lower-priority subscribers from running for that one dispatch — a clean way to implement "first match wins." - A subscriber class does nothing until it's registered as a tagged service (
{ name: event_subscriber }) in the module's.services.ymlfile — without that tag, Drupal never knows the class exists.
Coming up next
You now know how to listen for an event. But something has to actually announce that the event happened in the first place — and that something is a form controller you're about to meet. In the next lesson, we'll look at the other half of this exchange: how EventsExampleForm builds an event object and dispatches it through Drupal's event_dispatcher service, triggering everything you just read.