Dispatching Events in Drupal with the event_dispatcher Servicefor Drupal 11 , and 10

Last updated :  

In the last lesson you learned how to listen for an event. But nothing fires until something actually announces that the event happened — and that's what this lesson covers. We're going to look at the other half of the notice-board picture: the code that walks up and pins the note in the first place, using Drupal's event_dispatcher service.

What you'll learn in this lesson

  • How to inject the event_dispatcher service into a form (the same dependency-injection pattern you'll reuse constantly)
  • How to build a dedicated event object to carry contextual data along with the dispatch
  • How to actually call dispatch() and what happens the moment you do
  • Why Drupal insists on typed event classes instead of just passing an array of data

The source file

Path (relative to the Examples module's root): modules/events_example/src/Form/EventsExampleForm.php

<?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 SimpleForm form controller.
 *
 * The submitForm() method of this class demonstrates using the event dispatcher
 * service to dispatch an event.
 *
 * @ingroup events_example
 */
class EventsExampleForm extends FormBase {

  /**
   * The event dispatcher service.
   *
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  protected $eventDispatcher;

  /**
   * 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 and 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->eventDispatcher = $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['intro'] = [
      '#markup' => '<p>' . $this->t('This form demonstrates subscribing to, and dispatching, events. When the form is submitted an event is dispatched indicating a new report has been submitted. Event subscribers respond to this event with various messages depending on the incident type. Review the code for the events_example module to see how it works.') . '</p>',
    ];

    $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 objects are used to transport relevant data to any
    // subscribers, as well as keep track of the current state of an event. It
    // is best practice to create a unique class wrapping
    // \Symfony\Component\EventDispatcher\Event.
    $event = new IncidentReportEvent($type, $report);

    // Dispatch an event by specifying which event, and providing an event
    // object. Rather than hard code the event name you should use a constant
    // to represent the event being dispatched. The constant serves as a
    // location for documentation of the event, and ensures your code is future
    // proofed against event name changes.
    $this->eventDispatcher->dispatch($event, IncidentEvents::NEW_REPORT);
  }

}

How it works

FormBase, again

EventsExampleForm extends FormBase, the same base class you met back in the Form API topic — it's the version that supports dependency injection, which this lesson is all about putting to use.

Injecting the event_dispatcher service

public function __construct(EventDispatcherInterface $event_dispatcher) {
    $this->eventDispatcher = $event_dispatcher;
}

public static function create(ContainerInterface $container) {
    return new static(
        $container->get('event_dispatcher')
    );
}

This is the standard Drupal dependency-injection pattern you'll see used for services, controllers, blocks, and plugins alike: the constructor declares what it needs (typed as the interface, EventDispatcherInterface), and the static create() factory method — which Drupal always calls first — fetches the actual service instance from the container by its machine name, event_dispatcher, and hands it to the constructor. Drupal's concrete implementation behind that interface is ContainerAwareEventDispatcher, a performance-optimized dispatcher that compiles the list of subscribers into the service container at cache-build time, rather than scanning for them on every single dispatch.

Why not just call \Drupal::service('event_dispatcher') directly inside submitForm()? You could, technically — but constructor injection is the convention for any class (like a form) that supports it, because it makes dependencies explicit, testable, and swappable, instead of hidden inside a method body.

buildForm() — nothing new here

The form itself is deliberately simple: a radios element for the incident type, a textarea for a free-text description, and a submit button. You've built forms like this since the Form API topic — the interesting part of this class is entirely in submitForm().

submitForm() — where the dispatch happens

Three things happen here, in order.

1. Read the submitted values

$type = $form_state->getValue('incident_type');
$report = $form_state->getValue('incident');

Nothing new — getValue() pulls submitted data out by element key, exactly as you've done throughout the Form API topic.

2. Build a dedicated event object

$event = new IncidentReportEvent($type, $report);

Rather than passing $type and $report around as loose values, they get packaged into a purpose-built object, IncidentReportEvent. This is the data transport between the dispatcher and every subscriber that reacts. You'll see the full definition of that class in the next lesson, but the shape of the idea is simple: a typed object with documented getter methods (getType(), getReport()) is far easier and safer for a subscriber to work with than a raw, undocumented array.

3. Dispatch it

$this->eventDispatcher->dispatch($event, IncidentEvents::NEW_REPORT);

This one line is the entire "announce it to the world" step. dispatch() takes the event object and an event name — here, the constant IncidentEvents::NEW_REPORT, whose actual string value is 'events_example.new_incident_report'. The moment this line runs, the dispatcher looks up every subscriber registered for that exact event name and calls each one, in priority order, passing the same $event object to all of them — which is exactly the chain of methods you read about in the previous lesson.

Why a constant instead of a plain string?

You could technically write $this->eventDispatcher->dispatch($event, 'events_example.new_incident_report') directly. The reason not to: if that string ever needs to change, every single caller that hardcoded it would silently break. By routing every dispatch and every subscription through one shared constant, a rename becomes a one-line fix in a single file. This is a small habit worth adopting early — it costs nothing today and saves real debugging time later.

See it for yourself

Visit /examples/events-example on your DDEV site and submit the form. Every time you do, this exact code path runs: your submitted values get wrapped in an event object, and that object gets handed off to the dispatcher — which is precisely what produces the status message you see.

Status message shown after submitting the incident report form, confirming the dispatched event was handled

Quick check: if you deleted the line $event->stopPropagation() from every subscriber method you read in the previous lesson, but left everything in this file unchanged, what would change on screen? (You'd see all three status messages stack up instead of just one — dispatch() itself doesn't stop early; only a subscriber calling stopPropagation() does.)

Key takeaways

  • Inject the event_dispatcher service through the constructor and a create() factory method — don't reach for \Drupal::service('event_dispatcher') directly inside a class that already supports dependency injection.
  • Always wrap contextual data in a dedicated event class rather than passing a raw array — it gives subscribers a typed, documented, IDE-friendly API.
  • Call $dispatcher->dispatch($event, EventNameConstant) — the event object first, the event name second, always via a constant rather than a hardcoded string.
  • Dispatching is synchronous: every registered subscriber runs, in priority order, before dispatch() returns control to your code.
  • The same $event object is shared across every subscriber in the chain, so one subscriber really can affect what a later one sees — including stopping the chain outright.

Coming up next

You've now seen both halves of the exchange — dispatching and subscribing — but treated as two separate files. In the final lesson of this topic, we'll zoom out and look at all three (or four) pieces working together: the event name constants class, the event data object, the subscriber, and the services.yml registration that ties them all into Drupal's service container. By the end you'll be able to build this entire pattern from scratch in your own module.