Drupal 8 Hooks and the Symfony Event Dispatcher

Share this article

Key Takeaways

  • The adoption of many Symfony components in Drupal 8 indicates a shift away from traditional Drupalisms towards more modern PHP architectural decisions. This includes the gradual replacement of the hook system with plugins, annotations, and the Symfony Event Dispatcher component.
  • The Symfony Event Dispatcher component allows application components to communicate with each other by dispatching and listening to events. This provides a more flexible and decoupled approach to extending functionality compared to the procedural approach of hooks.
  • The Event Dispatcher is used in Drupal 8 by creating an event subscriber class that listens to a specific event. This class should implement the EventSubscriberInterface and define a ‘getSubscribedEvents’ method, which returns an array of events this class subscribes to.
  • Despite the shift towards the Event Dispatcher, hooks remain a vital part of Drupal 8, providing a means for modules to interact with the core system. However, their use has decreased as many have been replaced by plugins, annotations, and the Event Dispatcher component.
  • While hooks are still used in Drupal 8, it’s recommended to use the Symfony Event Dispatcher where possible, as it is more flexible and powerful, allowing for prioritization of event listeners, stopping event propagation, and modification of event data.
Please be aware that due to the development process Drupal 8 has been undergoing at the time of writing, some parts of the code might be outdated. Take a look at this repository in which I try to update the example code and make it work with the latest Drupal 8 release.

With the incorporation of many Symfony components into Drupal in its 8th version, we are seeing a shift away from many Drupalisms towards more modern PHP architectural decisions. For example, the both loved and hated hook system is getting slowly replaced. Plugins and annotations are taking away much of the need for info hooks and the Symfony Event Dispatcher component is replacing some of the invoked hooks. Although they remain strong in Drupal 8, it’s very possible that with Drupal 9 (or maybe 10) hooks will be completely removed.

In this article we are going to primarily look at how the Symfony Event Dispatcher component works in Drupal. Additionally, we will see also how to invoke and then implement a hook in Drupal 8 to achieve similar goals as with the former.

To follow along or to get quickly started, you can find all the code we work with here in this repository. You can just install the module and you are good to go. The version of Drupal 8 used is the first BETA release so it’s preferable to use that one to ensure compatibility. Alpha 15 should also work just fine. Let’s dive in.

What is the Event Dispatcher component?

A very good definition of the Event Dispatcher component can be found on the Symfony website:

The EventDispatcher component provides tools that allow your application components to communicate with each other by dispatching events and listening to them.

I recommend reading up on that documentation to better understand the principles behind the event dispatcher. You will get a good introduction to how it works in Symfony so we will not cover that here. Rather, we will see an example of how you can use it in Drupal 8.

Drupal 8 and the Event Dispatcher

For the better part of this article, we will focus on demonstrating the use of the Event Dispatcher in Drupal 8. To this end, we will create a simple demo module (event_dispatcher_demo) that has a configuration form which saves two values as configuration. Upon saving this form, we will dispatch an event that contains the config object and which will allow other parts of the application to intercept and modify it before being saved. Finally, we will do just that by demonstrating how to subscribe (or listen) to these events.

In Drupal 7, this type of modularity is only achieved with hooks. Hooks are being invoked and modules have the option to implement them and contribute with their own data. At the end of this article, we will see how to do that as well in Drupal 8. But first, let’s get on with our demo module.

If you don’t know the basics of Drupal 8 module development, I recommend checking out my previous articles in this series.

The form

The first thing we need is a simple config form with two fields. In a file called DemoForm.php located in the src/Form folder, we have the following:

<?php

/**
 * @file
 * Contains Drupal\event_dispatcher_demo\Form\DemoForm.
 */

namespace Drupal\event_dispatcher_demo\Form;

use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;

class DemoForm extends ConfigFormBase {

  /**
   * {@inheritdoc}
   */
  public function getFormID() {
    return 'demo_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $config = $this->config('event_dispatcher_demo.demo_form_config');
    $form['my_name'] = [
      '#type' => 'textfield',
      '#title' => $this->t('My name'),
      '#default_value' => $config->get('my_name'),
    ];
    $form['my_website'] = [
      '#type' => 'textfield',
      '#title' => $this->t('My website'),
      '#default_value' => $config->get('my_website'),
    ];
    return parent::buildForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {

    parent::submitForm($form, $form_state);

    $config = $this->config('event_dispatcher_demo.demo_form_config');

    $config->set('my_name', $form_state->getValue('my_name'))
      ->set('my_website', $form_state->getValue('my_website'));
      
    $config->save();
  }

}

Let’s also create a route for it (in the event_dispatcher_demo.routing.yml file) so we can access the form in the browser:

event_dispatcher_demo.demo_form:
  path: 'demo-form'
  defaults:
    _form: '\Drupal\event_dispatcher_demo\Form\DemoForm'
    _title: 'Demo form'
  requirements:
    _permission: 'access administration pages'

So now if you point your browser to example.com/demo-form, you should see the form. Submitting it will create and persist a configuration object called event_dispatcher_demo.demo_form_config that contains two fields: my_name and my_website .

The event dispatcher

Now it’s time to work on the form submit handler (the formSubmit() method) and dispatch an event when the form is saved. This is what the new method will look like:

public function submitForm(array &$form, FormStateInterface $form_state) {

  parent::submitForm($form, $form_state);

  $config = $this->config('event_dispatcher_demo.demo_form_config');

  $config->set('my_name', $form_state->getValue('my_name'))
    ->set('my_website', $form_state->getValue('my_website'));

  $dispatcher = \Drupal::service('event_dispatcher');

  $e = new DemoEvent($config);

  $event = $dispatcher->dispatch('demo_form.save', $e);

  $newData = $event->getConfig()->get();

  $config->merge($newData);

  $config->save();
}

So what happens here? After we take the submitted values and add them to the config object like before, we retrieve the event dispatcher object from the service container:

$dispatcher = \Drupal::service('event_dispatcher');

Please keep in mind that it’s recommended you inject this service into your class, but for brevity, we will retrieve it statically. You can read this article about dependency injection and the service container for more information.

Then we create a new DemoEvent object and pass it the $config through its constructor (we have not yet created the DemoEvent class, we will do that in a minute). Next, we use the dispatcher to dispatch an event of our type and assign this action the identifier demo_form.save. This will be used when subscribing to events (we’ll see this later). The dispatch() method returns the event object with modifications made to it so we can retrieve the config values that may or may not have been altered elsewhere and merge them into our original configuration. Finally, we save this object like we did initially.

Before moving onto the event subscription part of our application, let’s create the DemoEvent class we just instantiated above. In a file called DemoEvent.php located in the src/ folder of our module, we have the following:

<?php

/**
 * @file
 * Contains Drupal\event_dispatcher_demo\DemoEvent.
 */

namespace Drupal\event_dispatcher_demo;

use Symfony\Component\EventDispatcher\Event;
use Drupal\Core\Config\Config;

class DemoEvent extends Event {

  protected $config;

  /**
   * Constructor.
   *
   * @param Config $config
   */
  public function __construct(Config $config) {
    $this->config = $config;
  }

  /**
   * Getter for the config object.
   *
   * @return Config
   */
  public function getConfig() {
    return $this->config;
  }

  /**
   * Setter for the config object.
   *
   * @param $config
   */
  public function setConfig($config) {
    $this->config = $config;
  }

}

As you can see, this is a simple class that extends the default Event class and which defines setters and getters for the config object we will be passing around using this event. And since we created it, let’s also make sure we use it in the file where we defined the form:

use Drupal\event_dispatcher_demo\DemoEvent;

The event subscriber

Now that our form is functioning normally and an event is being dispatched when the form is saved, we should take advantage of that and subscribe to it. Let’s start with the event subscriber class that implements the EventSubscriberInterface. Inside a file called ConfigSubscriber.php (name of your choice) located in the src/EventSubscriber/ folder, we have the following:

<?php

/**
 * @file
 * Contains Drupal\event_dispatcher_demo\EventSubscriber\ConfigSubscriber.
 */

namespace Drupal\event_dispatcher_demo\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class ConfigSubscriber implements EventSubscriberInterface {

  static function getSubscribedEvents() {
    $events['demo_form.save'][] = array('onConfigSave', 0);
    return $events;
  }

  public function onConfigSave($event) {

    $config = $event->getConfig();

    $name_website = $config->get('my_name') . " / " . $config->get('my_website');
    $config->set('my_name_website', $name_website);
  }

}

So what happens here? The EventSubscriberInterface has only one required method called getSubscribedEvents(). This method is used to register events and callbacks to these events. So above we registered the callable onConfigSave() (found in the same class below) to the event dispatched with the identifier of demo_form.save. And in the callback method we simply add another value to the config object (based on a concatenation of the existing two values). The latter part is just for our demo purposes: here you can do what you want.

When we subscribed our onConfigSave() method to listen to the demo_form.save event, we passed a weight of 0. If you register multiple callbacks to the same event, this weight becomes important (the higher the number, the earlier it gets called). And if a callback alters the same values as one triggered before, they will get overridden. It’s good to keep this in mind.

Now in order for this event subscriber to work, we need to define it as a service and give it the event_subscriber tag. So in a file called event_dispatcher_demo.services.yml found in the root folder of our module, we will have this:

services:
  event_dispatcher_demo.config_subscriber:
    class: Drupal\event_dispatcher_demo\EventSubscriber\ConfigSubscriber
    tags:
      - { name: event_subscriber }

This is a simple service definition with the right tag that will make the container automatically instantiate an object of this class whenever the dispatcher is in play. And that is pretty much it. Clear the cache and if you now save the form again, the configuration object that gets saved will always contain a new value that is based on the first two.

Hooks

In the final part of this article we will demonstrate the use of hooks to achieve a similar goal.

First, let’s change the form submit handler and instead of dispatching events, we will invoke a hook and pass the config values to it. This is what the new submitForm() method will look like:

public function submitForm(array &$form, FormStateInterface $form_state) {

	parent::submitForm($form, $form_state);
	
	$config = $this->config('event_dispatcher_demo.demo_form_config');
	
	$config->set('my_name', $form_state->getValue('my_name'))
	  ->set('my_website', $form_state->getValue('my_website'));
	
	$configData = $config->get();
	$newData = \Drupal::service('module_handler')->invokeAll('demo_config_save', array($configData));
	
	$config->merge($newData);
	
	$config->save();
}

We are not using any event objects nor the dispatcher service. Instead, we retrieve the Module Handler service that contains the invokeAll() method used to invoke hook implementations from all modules. This is essentially replacing the Drupal 7 module_invoke_all() helper. And again, it is recommended to inject this service, but for brevity, we’ll retrieve it statically.

The hook implementation invoked in our case is hook_demo_config_save and it gets one parameter, an array of values pulled from our config object. Inside $newData we will have an array of values merged from all the implementations of this hook. We then merge that into our config object and finally save it.

Let’s quickly see an example hook implementation. As with Drupal 7, these can only be in .module files:

/**
 * Implements hook_demo_config_save().
 */
function event_dispatcher_demo_demo_config_save($configValues) {

  $configValues['my_name_website'] = $configValues['my_name'] . " / " . $configValues['my_website'];

  return $configValues;

}

As you can see, we are adding a new value to the config array that will later be merged into the object getting persisted. And we have essentially the same thing as we did with the event dispatcher.

Conclusion

In this article we have taken a look at how the Symfony Event Dispatcher component works in Drupal 8. We’ve learned how flexible it makes our application when it comes to allowing others to extend functionality. Additionally, we’ve seen how the invoked hooks work in the new version of Drupal. Not much has changed since Drupal 7 in this respect apart from the frequency with which they are used. Many hooks have been replaced by plugins and annotations and the Event Dispatcher component has also taken on a big chunk of what was in D7 a hook responsibility.

Although the Event Dispatcher approach is more verbose, it is the recommended way to go forward. Where possible, we no longer use the old procedural approach characteristic to hooks but rather object oriented, decoupled and testable solutions. And Symfony helps greatly with that.

Frequently Asked Questions (FAQs) about Drupal 8 Hooks and Symfony Event Dispatcher

What is the main difference between Drupal hooks and Symfony Event Dispatcher?

Drupal hooks and Symfony Event Dispatcher are both used to allow modules to interact with the core system. However, they operate differently. Drupal hooks are procedural and rely on a naming convention. When a certain event happens, Drupal will look for functions with a specific name and execute them. On the other hand, Symfony Event Dispatcher is object-oriented and uses a publish-subscribe pattern. It allows different components to communicate with each other by dispatching events and listening to them.

How can I implement a hook in Drupal 8?

To implement a hook in Drupal 8, you need to create a function in your module file with a specific name. The name should start with your module’s name, followed by the name of the hook. For example, if your module’s name is ‘example’ and you want to implement ‘hook_form_alter’, you would create a function named ‘example_form_alter’. This function will be automatically called when the form is altered.

How can I use Symfony Event Dispatcher in Drupal 8?

To use Symfony Event Dispatcher in Drupal 8, you need to create an event subscriber class that listens to a specific event. This class should implement the EventSubscriberInterface and define a ‘getSubscribedEvents’ method, which returns an array of events this class subscribes to. When the specified event is dispatched, the corresponding method in your class will be called.

Can I use both Drupal hooks and Symfony Event Dispatcher in the same module?

Yes, you can use both Drupal hooks and Symfony Event Dispatcher in the same module. However, it’s recommended to use Symfony Event Dispatcher when possible, as it’s more flexible and powerful. It allows you to prioritize event listeners, stop event propagation, and modify the event data.

What are the benefits of using Symfony Event Dispatcher over Drupal hooks?

Symfony Event Dispatcher offers several benefits over Drupal hooks. It’s object-oriented, which makes your code more organized and easier to test. It also allows you to prioritize event listeners, stop event propagation, and modify the event data. Moreover, it’s a part of the Symfony framework, which means you can use it in other Symfony-based projects.

How can I create a custom event in Drupal 8?

To create a custom event in Drupal 8, you need to create a new class that extends the Symfony\Component\EventDispatcher\Event class. This class can define any data or methods you need for your event. Then, you can dispatch this event using the event dispatcher service.

How can I listen to a custom event in Drupal 8?

To listen to a custom event in Drupal 8, you need to create an event subscriber class that subscribes to your custom event. This class should implement the EventSubscriberInterface and define a ‘getSubscribedEvents’ method, which returns an array with your custom event and the method to call when this event is dispatched.

How can I stop event propagation in Symfony Event Dispatcher?

To stop event propagation in Symfony Event Dispatcher, you can call the ‘stopPropagation’ method on the event object. Once this method is called, no further event listeners will be called.

How can I prioritize event listeners in Symfony Event Dispatcher?

To prioritize event listeners in Symfony Event Dispatcher, you can specify a priority when subscribing to an event. The priority is an integer, and the higher the number, the earlier the listener is called. If two listeners have the same priority, they are called in the order they were added.

Can I modify the event data in Symfony Event Dispatcher?

Yes, you can modify the event data in Symfony Event Dispatcher. The event object is passed by reference to the event listeners, so any changes you make to the object will be available to the subsequent listeners.

Daniel SiposDaniel Sipos
View Author

Daniel Sipos is a Drupal developer who lives in Brussels, Belgium. He works professionally with Drupal but likes to use other PHP frameworks and technologies as well. He runs webomelette.com, a Drupal blog where he writes articles and tutorials about Drupal development, theming and site building.

BrunoSdrupal 8drupal-planetdrupal8eventOOPHPPHPsymfonysymfony2
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week