Coding

Events and Event Subscribers in Drupal 8 and 9

Published by Alan Saunders on Wednesday, June 30, 2021 - 10:00

Reading Time: 9 minutes

Sections

Title
Introduction

Body

When Drupal 8 was being developed, there was a large shift towards introducing object oriented programming paradigms, so it was decided to implement the php framework symfony into Drupal 8. Amongst a lot of different ways of doing things, this change gave us the idea of Events and Event Subscribers, which is what we are going to talk about in this blog post.

Title
Events

Body

Events should hopefully be self explanatory, they are things that happen. So for example you can have an event for logging into a site.

Title
Event Subscribers

Body

Event subscribers on the other hand hook into an event and provide a reaction to that event. So for example, if you had an event for logging in to a site, you could have an event subscriber that on login does something like; If you have a certain user role, go a dashboard page instead of the users account page (/user). Obviously you could do that directly with a hook (hook_user_login).

Title
Events in action

Body

To show an an event in action, we'll create a custom module that has a custom event that triggers when someone logs into a website. To do this, you need the basic module skeleton as discussed in previous tutorials. First create a folder inside the custom modules folder, give it any name you like.

Title
[module name[.info.yml

Body

Now create a file called [module name].info.yml, as noted in previous tutorials, substitute the [module name] for the name you gave the folder. The contents of the info.yml file should look a bit like the code shown below.

name: 'Custom events and subscribers'
type: module
description: 'My Awesome Module'
core: 8.x
package: 'Custom'
core_version_requirement: ^8 || ^9

Title
User login event

Body

Next we add a folder called src inside the module folder with another folder called Event inside the src folder. Then we we create a file called UserLoginEvent.php. In this file we will add the following code to register our event.

In this class, we extend Symfony's Event Dispatcher Event class. Then we set a constant with our event name which is useful so that we don't have to repeat the event name which helps if we need to change the event name at any future point. Plus we also need it so that we can call our event name from our event subscriber.

Next in our constructor we access the account of the user who has logged in to the website, this is so that we can access the logged in users user object from our event subscriber.

<?php

namespace Drupal\custom_events_and_subscribers\Event;

use Drupal\user\UserInterface;
use Symfony\Contracts\EventDispatcher\Event;

/**
 * Event that is fired when a user logs in.
 */
class UserLoginEvent extends Event {

  const EVENT_NAME = 'custom_events_user_login';

  /**
   * The user account.
   *
   * @var \Drupal\user\UserInterface
   */
  public $account;

  /**
   * Constructs the object.
   *
   * @param \Drupal\user\UserInterface $account
   *   The account of the user logged in.
   */
  public function __construct(UserInterface $account) {
    $this->account = $account;
  }

}

Title
User login event subscriber

Body

Now onto our event subscriber, as noted earlier an event subscriber is the action that happens as a result of an event. To create an event subscriber, you need to create a new folder called EventSubscriber inside the src folder in your custom module, then create a file called for example UserLoginSubscriber.php. Our event subscriber should implement Symfonys Event Dispatcher Event Subscriber Interface, this means that we will need to implement all of the methods that the interface has in order for our event subscriber to function as we expect it too.

I have added the String Translation Trait in so that we can translate the string for security and multilingual reasons. In addition, I am using object oriented methods to give my class access to cores messenger and date formatter functionality. Messenger allows us to display messages on the page, date formatter allows us to format dates in a format of our choosing.

<?php

namespace Drupal\custom_events_and_subscribers\EventSubscriber;

use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\custom_events_and_subscribers\Event\UserLoginEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Drupal\Core\Messenger\MessengerInterface;

/**
 * Class UserLoginSubscriber.
 *
 * @package Drupal\custom_events\EventSubscriber
 */
class UserLoginSubscriber implements EventSubscriberInterface {
  use StringTranslationTrait;

  /**
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  private $messenger;

  /**
   * @var \Drupal\Core\Datetime\DateFormatterInterface
   */
  private $date_formatter;

  /**
   * LoginEventSubscriber constructor.
   *
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   */
  public function __construct(MessengerInterface $messenger, DateFormatterInterface $date_formatter) {
    $this->messenger = $messenger;
    $this->date_formatter = $date_formatter;
  }

}

Next we add a function that allows us to specify the events that our event subscriber should hook into. For our custom module, we want to hook into our custom login event, so inside the function getSubscribedEvents(), we specify our custom event class name then the constant that we set with the event name in, finally we specify the function in our event subscriber class that should be called when the event is triggered.

/**
   * @return array
   */
  public static function getSubscribedEvents() {
    return [
      UserLoginEvent::EVENT_NAME => 'onUserLogin',
    ];
  }

Now to the function specified above, this grabs the last login time and account name from the user object made available through our custom event class, we use the date formatter functionality to format the last login time using the 'short' date format as set in the Drupal 'Date and time formats' UI. We then use the messenger functionality to first output a welcome message outputting the users account name followed by outputting the last time they logged in in a user friendly format.

/**
 * Subscribe to the user login event dispatched.
 *
 * @param \Drupal\custom_events_and_subscribers\Event\UserLoginEvent $event
 *   Dat event object yo.
 */
public function onUserLogin(UserLoginEvent $event) {
  $last_logged_in = $this->date_formatter->format($event->account->getLastLoginTime(), 'short');
  $username = $event->account->getAccountName();

  if (empty($last_logged_in)) {
    $last_logged_in = 'Never';
  }

  $this->messenger
    ->addStatus($this->t('<strong>Hey there</strong>: %name.',
      [
        '%name' => $username,
      ]
    ))
    ->addStatus($this->t('<strong>You last logged in</strong>: %last_logged_in',
      [
        '%last_logged_in' => $last_logged_in
      ]
    ));
}

Title
[module name].services.yml

Body

Then we need to ensure that our custom module and ultimately Drupal knows about our new event subscriber, so we create a file in the root of our module called [module name].services.yml and we add the following code. You'll need to update custom_events_and_subscribers and UserLoginSubscriber if you have called them something else. Likewise if you are including different core services or other functionality then you'll need to update the arguments as needed.

services:
  # Subscriber to the event we dispatch in hook_user_login.
  custom_events_and_subscribers.user_login:
    class: Drupal\custom_events_and_subscribers\EventSubscriber\UserLoginSubscriber
    arguments: ['@messenger', '@date.formatter']
    tags:
      - { name: 'event_subscriber' }

Title
[module name].module

Body

We also need to create a file in the root of the custom module with the name of [module name].module, which is where you can place most of Drupals hooks.

There is a hook that triggers when someone logs into the site. In there we create an instance of our event and pass the user object of the user that is logging into the site, then we call Drupals event_dispatcher service and we pass the event name and the instance of our login event.

This should enable us to trigger our event and event subscriber when the user logs in.

/**
 * Implements hook_user_login().
 */
function custom_events_and_subscribers_user_login($account) {
  // Instantiate our event.
  $event = new UserLoginEvent($account);

  $event_dispatcher = \Drupal::service('event_dispatcher');
  $event_dispatcher->dispatch(UserLoginEvent::EVENT_NAME, $event);
}

Providing you have installed the module and you have no errors and status messages are set to show on the site, when you login to the site, a message should appear as shown in the below screenshot.

Image
Message shown on user login.

Title
Hook event dispatcher

Body

There is another way of hooking into the various process in Drupal that you would normally use hooks for like; hook_form_alter, hook_user_presave, hook_node_presave, hook_entity_insert etc. This alternative method enables us to use more object oriented code and leaves the heavy lifting of ensuring that our code is triggered on the correct hook for the module to do.

So this module doesn't get rid of the hooks, it just means that we don't need to implement any of the hooks and add the logic to hook into our event/ event subscriber. The module also means that we don't need to create any custom events, we just need to create an event subscriber.

You can find out more about the module at: https://www.drupal.org/project/hook_event_dispatcher. You would add the module the normal way using composer and install.

The module comes with a number of different sub modules depending on the event that you need/ want to hook into. In this blog post we'll look at a hook form alter. For this, you will need to enable the core event dispatcher submodule. You will also want to enable the main hook event dispatcher module too.

Title
Hook form alter event subscriber

Body

Next you should create a class inside the src/EventSubscriber folder in your custom module. In my case I have called my class HookFormAlterSubscriber.php, this class should extend the FormAlterEvent provided by the core_event_dispatcher module and implement Symfonys Event Subscriber Interface.

The contents of the constructor and create functions will depend on what functionality you require for altering the form that you wish to alter.

As mentioned earlier, you would then add a function for getSubscribedEvents, but instead of calling a custom event name, you would call the event name in one of the hook_event_dispatchers modules. In this case, we need to call the FORM_ALTER constant in the HookEventDispatcherInterface which is in the main hook_event_dispatcher module. You would then specify the name of the function that will perform the form alter, in our case we have called the function alterForm.

<?php

namespace Drupal\custom_events_and_subscribers\EventSubscriber;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\core_event_dispatcher\Event\Form\FormAlterEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\hook_event_dispatcher\HookEventDispatcherInterface;
use Drupal\Core\Routing\CurrentRouteMatch;

/**
 * Class HookFormAlterSubscriber.
 */
final class HookFormAlterSubscriber extends FormAlterEvent implements EventSubscriberInterface {

  /**
   * HookFormAlterSubscriber constructor.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   * @param \Drupal\Core\Routing\CurrentRouteMatch $route_match
   * @param EntityTypeManagerInterface $entity_type_manager
   */
  public function __construct(ConfigFactoryInterface $config_factory, CurrentRouteMatch $route_match, EntityTypeManagerInterface $entity_type_manager) {
    $this->configFactory = $config_factory;
    $this->routeMatch = $route_match;
    $this->entityTypeManager = $entity_type_manager;
  }

  /**
   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
   *
   * @return \Drupal\custom_events_and_subscribers\EventSubscriber\HookFormAlterSubscriber
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('current_route_match'),
      $container->get('entity_type.manager'),
      $container->get('config.factory')
    );
  }

  /**
   * @return array
   */
  static function getSubscribedEvents() {
    return [
      HookEventDispatcherInterface::FORM_ALTER => 'alterForm'
    ];
  }

  /**
   * @param FormAlterEvent $event
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function alterForm(FormAlterEvent $event) {
      $form = &$event->getForm();
      $form_state = $event->getFormState();
      // Form altering logic happens here.
      $event->getFormId();
  }
}

Example code from my event subscriber is shown above. In the alterForm function, there is a parameter of $event which should be an instance of the FormAlterEvent. In a normal hook form alter. You would have access to three variables; $form, $form_state and $form_id, just like when using the hook form alter hook conventionally. you can access the form by calling &$event->getForm();, $form_state by calling $event->getFormState(); and you can get the form id by calling $event->getFormId();. You would then simply add conditional logic to check the form id and alter the form by adding new fields, hiding fields etc mostly the usual manner. You can use the usual methods of using devel or other php debug functions to see is contained in these variables so that you can navigate to where you need to be to do what you need to do.

The reason why an & is infront of the event variable for the event is:

"It passes a reference to the variable so when any variable assigned the reference is edited, the original variable is changed. They are really useful when making functions which update an existing variable. Instead of hard coding which variable is updated, you can simply pass a reference to the function instead."

Title
[module name].services.yml

Body

Just like the previous event subscriber, you would need to add an entry for this class into your [module name].services.yml file, so that the event subscriber will be picked up and used by Drupal. The parameters will change depending on your requirements.

services:
  custom_events_and_subscribers.hook_form_alter:
    class: Drupal\custom_events_and_subscribers\EventSubscriber\HookFormAlterSubscriber
    arguments: ['@config.factory', '@current_route_match', '@entity_type.manager']
    tags:
      - { name: event_subscriber }
Categories:

Related blog posts

Logging stuff in Drupal 7/8/9

Authored by Alan Saunders on Sunday, January 24, 2021 - 22:00
Reading Time: 6 minutes

Building a Drupal 8/9 custom block

Authored by Alan Saunders on Sunday, January 3, 2021 - 10:00
Reading Time: 6 minutes

Theme debug in Drupal 7/8/9

Authored by Alan Saunders on Saturday, April 17, 2021 - 22:00
Reading Time: 2 minutes