Event System
Overview
Events allow different components of the system to interact and communicate with each other. One system component dispatches the event at an appropriate time; many events are dispatched by Drupal core and the Symfony event system in every request. Other system components can register as event subscribers. When an event is dispatched, a method is called on each registered subscriber, allowing each one to react. For more on the general concept of events, see The EventDispatcher Component in the Symfony docs
Take an example from the HttpKernel component. Once a Response
object has been created, it may be useful to allow other elements in the system to modify it (e.g. add some cache headers) before it's actually used. To make this possible, the Symfony kernel dispatches an event - kernel.response
. Here's how it works:
- A listener (PHP object) tells a central dispatcher object that it wants to listen to the
kernel.response
event; - At some point, the Symfony kernel tells the dispatcher object to dispatch the
kernel.response
event, passing with it anEvent
object that has access to the Response object; - The dispatcher notifies (i.e. calls a method on) all listeners of the
kernel.response
event, allowing each of them to make modifications to theResponse
object.
Event systems are used in many complex applications as a way to allow extensions to modify how the system works. An event system can be implemented in a variety of ways, but generally, the concepts and components that make up the system are the same.
- Event Subscribers - Sometimes called "Listeners", are callable methods or functions that react to an event being propagated throughout the Event Registry.
- Event Registry - Where event subscribers are collected and sorted.
- Event Dispatcher - The mechanism in which an event is triggered, or "dispatched", throughout the system.
- Event Context - Many events require a specific set of data that is important to the subscribers to an event. This can be as simple as a value passed to the Event Subscriber, or as complex as a specially created class that contains the relevant data.
Finding Drupal events
There are several ways to find events to subscribe to:
- Search
web/core
for@Event
Look in
vendor/symfony/http-kernel/KernelEvents.php
(e.g. forKernelEvents::REQUEST
)andweb/core/lib/Drupal/Core/Config/ConfigEvents.php
(e.g. forConfigEvents::SAVE
)You can also see a listing at the bottom of this page
You can also use the webprofiler module to view events and the event subscribers. Enable it and check the checkbox in it's settings for events. When you view a page, you’ll see the toolbar at the bottom of the page. Click any link and it will give you useful stats. Select
events
on the left. and you will see a long list of events and the event subscribers that are called. e.g. in the first line below, the dispatched event is “kernel.request” which is listened to by Drupal/Core/Routing/RoutePreloader.php::onRequest
Generate event subscriber with Drush
You can use drush to generate the code for working on this module using drush generate service:event-subscriber
It will create the module.info.yml
file, the module.services.yml
file and also the src/EventSubscriber/EventsExampleSubscriber.php
files for you.
Subscribe to a core event
Here is an example which subscribes to the Kernel::REQUEST
event. This happens very early in the process. On each request, it checks to see if the site is in maintenance mode, and if it is, logs the event and redirects the user to www.nytimes.com
.
Note
You can use drush to generate the code for working on this module using drush generate service:event-subscriber
In web/modules/custom/mymodule/mymodule.info.yml
name: 'Mymodule'
type: module
description: 'Exploring events'
package: 'Custom'
core_version_requirement: ^10
In web/modules/custom/mymodule/mymodule.services.yml
services:
mymodule.route_finished_subscriber:
class: Drupal\mymodule\EventSubscriber\RouteFinishedSubscriber
arguments:
- '@state'
tags:
- { name: event_subscriber }
And finally in web/modules/custom/mymodule/src/EventSubscriber/RouteFinishedSubscriber.php
<?php declare(strict_types = 1);
namespace Drupal\mymodule\EventSubscriber;
use Drupal\Core\Routing\TrustedRedirectResponse;
use Drupal\Core\State\StateInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Redirects to a specific URL when the site is in maintenance mode.
*/
final class RouteFinishedSubscriber implements EventSubscriberInterface {
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* Constructs a new RouteFinishedSubscriber.
*
* @param \Drupal\Core\State\StateInterface $state
* The state service.
*/
public function __construct(StateInterface $state) {
$this->state = $state;
}
/**
* Redirect to nytimes.com when the site is in maintenance mode and logs event.
*
* @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
* The event to respond to.
*/
public function onKernelRequest(RequestEvent $event): void {
if ($this->state->get('system.maintenance_mode')) {
$response = new TrustedRedirectResponse('https://www.nytimes.com');
\Drupal::logger('mymodule')->info("System in maint mode - sent them to the times!");
$event->setResponse($response);
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
KernelEvents::REQUEST => ['onKernelRequest'],
];
}
}
The getSubscribedEvents()
method from vendor/symfony/event-dispatcher/EventSubscriberInterface.php
returns an array of event names this subscriber wants to listen to.
The array keys are event names and the value can be:
- The method name to call (priority defaults to 0)
- An array composed of the method name to call and the priority
- An array of arrays composed of the method names to call and respective priorities, or 0 if unset
For example:
['eventName' => 'methodName']
['eventName' => ['methodName', $priority]]
['eventName' => [['methodName1', $priority], ['methodName2']]]
The code must not depend on runtime state as it will only be called at compile time. All logic depending on runtime state must be put into the individual methods handling the events. Returns: array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>
Define custom events
- Define the event constants in a class
- Define the event class
- Dispatch the event
When creating a custom event, first create a file mymodule/src/Event/IncidentEvents.php
to hold the constants that define the event names like this:
<?php
namespace Drupal\mymodule\Event;
final class IncidentEvents {
/**
* Name of the event fired when a new report is created.
*
* @Event
*
* @var string
*/
const NEW_REPORT = 'mymodule.new_report';
}
In the example above, we define an event called NEW_REPORT
. We then subscribe to this event in the getSubscribedEvents
method of the EventsExampleSubscriber
class.
There is another example in the Examples module's events_example module.
Then to handle the additional contextual data that you want to provide to the event subscribers when dispatching an event, create a new class that extends \Symfony\Component\EventDispatcher\Event
mymodule/src/Event/IncidentReportEvent.php
.
<?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.
*
*/
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;
}
}
See the code in the example module
To dispatch your event, you can use:
$event = new IncidentReportEvent($type, $report);
$this->event_dispatcher->dispatch($event, IncidentEvents::NEW_REPORT);
Or more completely:
<?php
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Drupal\mymodule\Event\NewReportEvent;
class SomeClass {
protected $eventDispatcher;
public function __construct(EventDispatcherInterface $event_dispatcher) {
$this->eventDispatcher = $event_dispatcher;
}
public function someMethod() {
$report = 'some report data';
$event = new NewReportEvent($report);
$this->eventDispatcher->dispatch($event, NewReportEvent::EVENT_NAME);
}
}
Subscribe to custom events
You need an Event Subscriber class which is the class that responds to the custom event (i.e. the class that does the magic when the event fires). You also need a mymodule.services.yml
which tells Drupal about the Event Subscriber class.
The EventSubscriber class e.g. mymodule/src/EventSubscriber/EventsExampleSubscriber.php
looks something like this:
<?php
namespace Drupal\mymodule\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Drupal\mymodule\Event\IncidentEvents;
class YourModuleEventSubscriber implements EventSubscriberInterface {
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[IncidentEvents::NEW_REPORT][] = ['notifyMario'];
return $events;
}
/**
* Method to handle the notifyMario event.
*/
public function notifyMario($event) {
// You put your code here to handle the event.
}
}
Then you register your event subscriber in mymodule.services.yml
like this:
services:
your_module.event_subscriber:
class: Drupal\your_module\EventSubscriber\YourModuleEventSubscriber
tags:
- { name: event_subscriber }
If you want to subscribe to multiple events, you can add more entries to the $events
array in the getSubscribedEvents
method. The key is the event name and the value is an array of method names to call when the event is triggered. You can also specify a priority for each method. The method with the highest priority will be called first. If two methods have the same priority, they will be called in the order they were added to the array.
$events[IncidentEvents::NEW_REPORT][] = ['notifyMario'];
$events[IncidentEvents::NEW_REPORT][] = ['notifyBatman', -100];
$events[IncidentEvents::NEW_REPORT][] = ['notifyDefault', -255];
Dispatch a custom event
To dispatch a custom event, instantiate a new event object and call the event_dispatcher->dispatch()
method. In the example below, the event is dispatched by the user filling out a form. Here is the submitForm
method.
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$type = $form_state->getValue('incident_type');
$report = $form_state->getValue('incident');
// When dispatching or triggering an event, start by constructing a new
// event object. Then use the event dispatcher service to notify any event
// subscribers.
$event = new IncidentReportEvent($type, $report);
// Dispatch an event by specifying which event, and providing an event
// object that will be passed along to any subscribers.
$this->event_dispatcher->dispatch($event, IncidentEvents::NEW_REPORT);
Here is the entire file for clarity:
<?php
namespace Drupal\events_example\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Drupal\events_example\Event\IncidentEvents;
use Drupal\events_example\Event\IncidentReportEvent;
/**
* Implements the EventsExampleForm form controller.
*
* The submitForm() method of this class demonstrates using the event dispatcher
* service to dispatch an event.
*
* @see \Drupal\events_example\Event\IncidentEvents
* @see \Drupal\events_example\Event\IncidentReportEvent
* @see \Symfony\Component\EventDispatcher\EventDispatcherInterface
* @see \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher
*
* @ingroup events_example
*/
class EventsExampleForm extends FormBase {
/**
* The event dispatcher service.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $event_dispatcher;
/**
* Constructs a new UserLoginForm.
*
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher service.
*/
public function __construct(EventDispatcherInterface $event_dispatcher) {
// The event dispatcher service is an implementation of
// \Symfony\Component\EventDispatcher\EventDispatcherInterface. In Drupal
// this is generally an instance of the
// \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher service.
// This dispatcher improves performance when dispatching events by compiling
// a list of subscribers into the service container so that they do not need
// to be looked up every time.
$this->event_dispatcher = $event_dispatcher;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('event_dispatcher')
);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['incident_type'] = [
'#type' => 'radios',
'#required' => TRUE,
'#title' => $this->t('What type of incident do you want to report?'),
'#options' => [
'stolen_princess' => $this->t('Missing princess'),
'cat' => $this->t('Cat stuck in tree'),
'joker' => $this->t('Something involving the Joker'),
],
];
$form['incident'] = [
'#type' => 'textarea',
'#required' => FALSE,
'#title' => $this->t('Incident report'),
'#description' => $this->t('Describe the incident in detail. This information will be passed along to all crime fighters.'),
'#cols' => 60,
'#rows' => 5,
];
$form['actions'] = [
'#type' => 'actions',
];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Submit'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'events_example_form';
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$type = $form_state->getValue('incident_type');
$report = $form_state->getValue('incident');
// When dispatching or triggering an event, start by constructing a new
// event object. Then use the event dispatcher service to notify any event
// subscribers.
$event = new IncidentReportEvent($type, $report);
// Dispatch an event by specifying which event, and providing an event
// object that will be passed along to any subscribers.
$this->event_dispatcher->dispatch($event, IncidentEvents::NEW_REPORT);
}
}
Stop propagation and access information about the event
If you have multiple subscribers to an event and you want to skip them after a certain one is called, you can use $event->stopPropagation
:
public function notifyMario(IncidentReportEvent $event) {
// You can use the event object to access information about the event passed
// along by the event dispatcher.
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__]));
// Optionally use the event object to stop propagation.
// If there are other subscribers that have not been called yet this will
// cause them to be skipped.
$event->stopPropagation();
}
}
Use custom autocomplete route
The RouteSubscriber
class extends RouteSubscriberBase
and listens to dynamic route events. It serves to alter existing routes via the alterRoutes
method. The RouteCollection
object parameter contains all the routes defined in the application.
In the web/modules/custom/abc_admin_enhancements/abc_admin_enhancements.services.yml
file there is a service defined for the RouteSubscriber
class. This service is tagged as an event subscriber, which means that it will be automatically registered with the event dispatcher when the module is installed. The RouteSubscriber
class listens for the system.entity_autocomplete
route and alters it to use a custom controller.
services:
abc_admin_enhancements.route_subscriber:
class: Drupal\abc_admin_enhancements\Routing\RouteSubscriber
tags:
- { name: event_subscriber }
abc_admin_enhancements.autocomplete_matcher:
class: Drupal\abc_admin_enhancements\EntityAutocompleteMatcher
arguments: ['@plugin.manager.entity_reference_selection']
Here is the web/modules/custom/abc_admin_enhancements/src/Routing/RouteSubscriber.php
file:
<?php
namespace Drupal\abc_admin_enhancements\Routing;
use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;
/**
* Listens to the dynamic route events.
*/
class RouteSubscriber extends RouteSubscriberBase {
/**
* {@inheritdoc}
*/
public function alterRoutes(RouteCollection $collection) {
if($route = $collection->get('system.entity_autocomplete')) {
$route->setDefault('_controller', '\Drupal\abc_admin_enhancements\Controller\EntityAutocompleteController::handleAutocomplete');
}
}
}
Here is web/modules/custom/abc_admin_enhancements/src/Controller/EntityAutocompleteController.php
:
<?php
namespace Drupal\abc_admin_enhancements\Controller;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\abc_admin_enhancements\EntityAutocompleteMatcher;
use Symfony\Component\DependencyInjection\ContainerInterface;
class EntityAutocompleteController extends \Drupal\system\Controller\EntityAutocompleteController {
/**
* The autocomplete matcher for entity references.
*/
protected $matcher;
/**
* {@inheritdoc}
*/
public function __construct(EntityAutocompleteMatcher $matcher, KeyValueStoreInterface $key_value) {
$this->matcher = $matcher;
$this->keyValue = $key_value;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('abc_admin_enhancements.autocomplete_matcher'),
$container->get('keyvalue')->get('entity_autocomplete')
);
}
}
Finally, here is the web/modules/custom/abc_admin_enhancements/src/EntityAutocompleteMatcher.php
file with the getMatches
method:
<?php
namespace Drupal\abc_admin_enhancements;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Tags;
class EntityAutocompleteMatcher extends \Drupal\Core\Entity\EntityAutocompleteMatcher {
/**
* Gets matched labels based on a given search string.
*/
public function getMatches($target_type, $selection_handler, $selection_settings, $string = '') {
$account = \Drupal::currentUser();
$user_roles = $account->getRoles(true);
$is_admin = in_array('administrator', $user_roles);
$is_pcm = in_array('principal_content_manager', $user_roles);
$is_lcm = in_array('local_content_manager', $user_roles);
$is_lcm_lite = in_array('local_light_content_manager', $user_roles);
$is_sce = in_array('staff_content_editor', $user_roles);
$is_pla = in_array('job_first_approver', $user_roles) || in_array('job_second_approver', $user_roles);
$matches = [];
$options = $selection_settings + [
'target_type' => $target_type,
'handler' => $selection_handler,
];
$handler = $this->selectionManager->getInstance($options);
if (isset($string)) {
// Get an array of matching entities.
$match_operator = !empty($selection_settings['match_operator']) ? $selection_settings['match_operator'] : 'CONTAINS';
$entity_labels = $handler->getReferenceableEntities($string, $match_operator, 10);
// Loop through the entities and convert them into autocomplete output.
foreach ($entity_labels as $entity_type => $values) {
// Filter results to only editable labs for LCM's...
if($entity_type === 'laboratory' && ($is_lcm || $is_lcm_lite)) {
$user = \Drupal\user\Entity\User::load($account->id());
$editable_labs = [];
if($user->hasField('field_editable_labs')) {
foreach($user->get('field_editable_labs')->getValue() as $editable_lab) {
$editable_labs[] = $editable_lab['target_id'];
}
}
$values = array_filter($values, function($key) use($editable_labs) {
return in_array($key, $editable_labs);
}, ARRAY_FILTER_USE_KEY);
}
foreach ($values as $entity_id => $label) {
/*$entity = \Drupal::entityTypeManager()->getStorage($target_type)->load($entity_id);
if(!$entity->isPublished()) {
// Filter out unpublished content.
continue;
}*/
$key = "{$label} ({$entity_id})";
// Strip things like starting/trailing white spaces, line breaks and
// tags.
$key = preg_replace('/\\s\\s+/', ' ', str_replace("\n", '', trim(Html::decodeEntities(strip_tags($key)))));
// Names containing commas or quotes must be wrapped in quotes.
$key = Tags::encode($key);
$matches[] = array(
'value' => $key,
'label' => $label,
);
}
}
}
return $matches;
}
}