You've now seen dispatching and subscribing as two separate lessons. In reality, they're not separate at all — they're four small, coordinated pieces of a single system, and none of them do anything on their own. This lesson puts all four side by side so you can see the whole picture, and gives you the one piece we skipped: services.yml, the file that actually connects a subscriber class to Drupal's event dispatcher.
By the end of this lesson you'll be able to build this entire pattern — a custom event, start to finish — in your own module.
What you'll learn in this lesson
- How
services.ymlregisters a class as a taggedevent_subscriberservice, and why that tag is non-negotiable - The full definition of
IncidentEvents, the constants class that names the event - The full definition of
IncidentReportEvent, the data object that carries context to subscribers - How to see all four pieces (services.yml, event name, event data, subscriber) as one coordinated system
The four pieces, at a glance
Every custom event in Drupal is built from the same four ingredients. Here they are, mapped to the files you're about to read:
- A name —
src/Event/IncidentEvents.php, a small constants class - A payload —
src/Event/IncidentReportEvent.php, the object carrying data to subscribers - A trigger —
src/Form/EventsExampleForm.php, which you already read in the previous lesson - A reaction —
src/EventSubscriber/EventsExampleSubscriber.php, which you already read two lessons ago
We'll add the missing piece that wires the reaction into Drupal in the first place — events_example.services.yml — then look at the two small classes that give the event its name and its data.
File 1: events_example.services.yml
Path: modules/events_example/events_example.services.yml
# Subscribing to an event requires you to create a new service tagged with the
# 'event_subscriber' tag. This tells the service container, and by proxy the
# event dispatcher service, that the class registered here can be queried to get
# a list of events that it would like to be notified about.
#
# For more on defining and tagging services see
# https://api.drupal.org/api/drupal/core%21core.api.php/group/container/8.2.x
services:
# Give your service a unique name; the convention is to prefix service names
# with the name of the module that implements them.
events_example_subscriber:
# Point to the class that will contain your implementation of
# \Symfony\Component\EventDispatcher\EventSubscriberInterface
class: Drupal\events_example\EventSubscriber\EventsExampleSubscriber
tags:
- {name: event_subscriber}
The services: root key
Every service definition in Drupal lives under this one top-level key. Drupal scans every *.services.yml file across all enabled modules while building the service container, and registers whatever it finds. This is the same mechanism that gives you access to services like current_user, entity_type.manager, or the event_dispatcher you injected in the last lesson — they were all declared exactly this way, in core's own .services.yml files.
Service ID: events_example_subscriber
This string is the service's unique machine name across the entire site — which is exactly why the convention is to prefix it with your module's name. Two modules both naming a service subscriber would collide; events_example_subscriber and my_other_module_subscriber never will.
class:
This tells the container exactly which PHP class to instantiate when the service is needed — here, the EventsExampleSubscriber class you already read in the first lesson of this topic.
The tags: key — the part that actually matters
This is the single most important line in the whole file. Tagging a service with {name: event_subscriber} is what tells Symfony's event dispatcher component (which Drupal uses internally) to call getSubscribedEvents() on this class and register everything it returns. Without this tag, the class still exists, is still perfectly valid PHP, and does absolutely nothing — Drupal has no way of discovering it as a listener.
event_subscriber tag (or misspelling it), and then spending twenty minutes wondering why nothing happens. If your subscriber never seems to fire, this file — and this exact tag — is the first thing worth double-checking.File 2: src/Event/IncidentEvents.php
This is the constants class referenced in both previous lessons as IncidentEvents::NEW_REPORT. Here it is in full:
<?php
namespace Drupal\events_example\Event;
/**
* Defines events for the events_example module.
*
* It is best practice define the unique names for events as constants on a
* class. This provides a place for documentation of the events. As well as
* allowing the event dispatcher to use the constants instead of hard coding a
* string.
*
* In this example we're defining one new event:
* 'events_example.new_incident_report'. This event will be dispatched by the
* form controller \Drupal\events_example\Form\EventsExampleForm whenever a new
* incident is reported. If your application dispatches more than one event
* you can use a single class to document multiple events. Just add a new
* constant for each. Group related events together with a single class, define
* another class for unrelated events.
*
* The docblock for each event constant should contain an "@Event" tag. This is
* used to ensure documentation parsing tools can gather and list all events.
* For example,
* https://api.drupal.org/api/drupal/core%21core.api.php/group/events/
*
* The docblock should also contain a description of when, and
* under what conditions, the event is triggered. A module developer should be
* able to read this description in order to determine whether or not this is
* the event that they want to subscribe to.
*
* This class is declared as final so that it can not be extended. It should
* only ever be used to provide unique event names, and documentation.
*
* In core \Drupal\Core\Config\ConfigCrudEvent is a good example of defining and
* documenting new events.
*
* @see \Drupal\Core\Config\ConfigCrudEvent
*
* @ingroup events_example
*/
final class IncidentEvents {
/**
* Name of the event fired when a new incident is reported.
*
* This event allows modules to perform an action whenever a new incident is
* reported via the incident report form. The event listener method receives a
* \Drupal\events_example\Event\IncidentReportEvent instance.
*
* @Event
*
* @see \Drupal\events_example\Event\IncidentReportEvent
*
* @var string
*/
const NEW_REPORT = 'events_example.new_incident_report';
}
final class IncidentEvents
Marking this class final is intentional: it prevents anyone from subclassing it. The class exists for exactly one purpose — to be a constants container and a documentation anchor — and nothing about that purpose benefits from inheritance.
const NEW_REPORT = 'events_example.new_incident_report'
The actual event name is a plain string, and like a service ID, it needs to be globally unique — hence the events_example. prefix. Every place that dispatches or subscribes to this event references IncidentEvents::NEW_REPORT rather than typing the raw string, so a future rename only ever needs to happen in this one file.
The @Event docblock tag
This annotation signals to documentation tooling (and to any developer reading the source) that this constant represents a real, dispatchable event — worth cross-referencing from api.drupal.org-style generated docs. It's a convention, not something Drupal enforces at runtime, but it's one worth following in your own modules so other developers can discover what events you offer.
File 3: src/Event/IncidentReportEvent.php
This is the event object you saw being constructed in the previous lesson as new IncidentReportEvent($type, $report). Here's its full definition:
<?php
namespace Drupal\events_example\Event;
use Symfony\Contracts\EventDispatcher\Event;
/**
* Wraps a incident report event for event subscribers.
*
* Whenever there is additional contextual data that you want to provide to the
* event subscribers when dispatching an event you should create a new class
* that extends \Symfony\Component\EventDispatcher\Event.
*
* See \Drupal\Core\Config\ConfigCrudEvent for an example of this in core.
*
* @see \Drupal\Core\Config\ConfigCrudEvent
*
* @ingroup events_example
*/
class IncidentReportEvent extends Event {
/**
* Incident type.
*
* @var string
*/
protected $type;
/**
* Detailed incident report.
*
* @var string
*/
protected $report;
/**
* Constructs an incident report event object.
*
* @param string $type
* The incident report type.
* @param string $report
* A detailed description of the incident provided by the reporter.
*/
public function __construct($type, $report) {
$this->type = $type;
$this->report = $report;
}
/**
* Get the incident type.
*
* @return string
* The type of report.
*/
public function getType() {
return $this->type;
}
/**
* Get the detailed incident report.
*
* @return string
* The text of the report.
*/
public function getReport() {
return $this->report;
}
}
extends Event
Extending Symfony's Event base class is what gives every event object — including this one — the stopPropagation() method you used throughout the first lesson of this topic. You don't have to implement that method yourself; it comes free with the base class.
Protected properties, public getters
Notice that $type and $report are protected, not public — a subscriber can only read them through getType() and getReport(), never overwrite them directly. This is a deliberate, small piece of API design: the object that dispatches the event fully owns its data, and subscribers can inspect it but not silently mutate what the next subscriber in the chain will see.
__construct($type, $report)
All contextual data is supplied at construction time — by whichever code dispatches the event, which in this case is the form controller's submitForm() method you read in the previous lesson. That guarantees every subscriber that ever receives this object gets a fully-populated one; there's no possibility of a subscriber seeing a half-built event.
Putting it all together
Here's the full lifecycle, start to finish, in the order things actually execute:
- You visit
/examples/events-exampleand submit the form with an incident type. EventsExampleForm::submitForm()reads the form values and constructs a newIncidentReportEvent($type, $report).- It calls
$this->eventDispatcher->dispatch($event, IncidentEvents::NEW_REPORT). - Because
events_example.services.ymltaggedEventsExampleSubscriberas anevent_subscriber, Drupal already knows to ask it, viagetSubscribedEvents(), which events it cares about — and it said it cares aboutIncidentEvents::NEW_REPORT. - The dispatcher calls
notifyMario(), then (if not stopped)notifyBatman(), then (if still not stopped)notifyDefault()— each receiving the exact same$eventobject. - Whichever method reacts calls
$this->messenger()->addStatus(...), which is the message you see rendered on the page.
See it for yourself
Visit /examples/events-example again and submit the form once more — this time picture all six steps above happening, in order, in the moment between clicking Submit and the page reloading with a message.
Quick check: name the one file, of the four covered across this whole topic, that would need editing if you wanted to rename this event from
NEW_REPORTto something else, and have every dispatcher and subscriber automatically pick up the new name. (IncidentEvents.php— everything else references the constant, never the raw string.)
Key takeaways
- A custom event system needs four coordinated pieces: a constants class naming the event, a data class carrying its payload, code that dispatches it, and a subscriber class that reacts — plus a
services.ymlentry tying the subscriber into the container. - Tagging a service with
{name: event_subscriber}in*.services.ymlis what actually makes Drupal's event dispatcher discover and call a subscriber class — skip this and the class is inert. - Event name constants belong on a
finalclass with an@Eventdocblock tag — this is a strong community convention worth following even though nothing enforces it at runtime. - Event data objects should extend Symfony's
Eventbase class, expose data through public getters over protected properties, and populate everything at construction time. - Once you can build this four-piece pattern from scratch, you have a genuinely reusable tool: any time two parts of a Drupal site need to communicate without a hard dependency on each other, this is the mechanism to reach for.
Coming up next
You now understand one of the two major architectural patterns that hold Drupal together — events. The other is the one that actually powers blocks (which you built back in Topic 3), field types, and a huge portion of Drupal's own extensibility: the Plugin system. In the next topic, we'll pull back the curtain on how Drupal discovers, defines, and manages plugins — the pattern you've technically already been using without knowing its name.