Book Image

Drupal 9 Module Development - Third Edition

By : Daniel Sipos
Book Image

Drupal 9 Module Development - Third Edition

By: Daniel Sipos

Overview of this book

With its latest release, Drupal 9, the popular open source CMS platform has been updated with new functionalities for building complex Drupal apps with ease. This third edition of the Drupal Module Development guide covers these new Drupal features, helping you to stay on top of code deprecations and the changing architecture with every release. The book starts by introducing you to the Drupal 9 architecture and its subsystems before showing you how to create your first module with basic functionality. You’ll explore the Drupal logging and mailing systems, learn how to output data using the theme layer, and work with menus and links programmatically. Once you’ve understood the different kinds of data storage, this Drupal guide will demonstrate how to create custom entities and field types and leverage the Database API for lower-level database queries. You’ll also learn how to introduce JavaScript into your module, work with various file systems, and ensure that your code works on multilingual sites. Finally, you’ll work with Views, create automated tests for your functionality, and write secure code. By the end of the book, you’ll have learned how to develop custom modules that can provide solutions to complex business problems, and who knows, maybe you’ll even contribute to the Drupal community!
Table of Contents (20 chapters)
3
Chapter 3: Logging and Mailing

Event Dispatcher and redirects

A common thing you'll have to do as a module developer is to intercept a given request and redirect it to another page, and more often than not, this will have to be dynamic, depending on the current user or other contextual info. What we have to do in order to achieve this is subscribe to the kernel.request event (remember this from the previous chapter?) and then change the response directly. However, before seeing an example of this, let's take a look at how we can perform a simpler redirect from within a Controller. You know, since we're on the subject.

Redirecting from a Controller

In this chapter, we wrote a Controller that returns a render array. We know from the previous chapter that this is picked up by the theme system and turned into a response. In Chapter 4, Theming, we will go into a bit more detail and see how this process is done. However, this render pipeline can also be bypassed if the Controller returns a response directly. Let's consider the following example:

return new \Symfony\Component\HttpFoundation\Response('my text');  

This will bypass much of that processing and return a blank white page with only the "my text" string on it. The Response class we're using is from the Symfony HTTP Foundation component.

However, we also have a handy RedirectResponse class that we can use, and it will redirect the browser to another page:

return new \Symfony\Component\HttpFoundation\RedirectResponse('/node/1');  

The first parameter is the URL where we want to redirect to. Typically, this should be an absolute URL; however, browsers nowadays are smart enough to handle a relative path as well. So, in this case, the Controller will redirect us to that path.

Note

Typically, when returning redirect responses, you'll want to use a child class of RedirectResponse. For example, we have the LocalRedirectResponse and TrustedRedirectResponse classes, which both extend from SecuredRedirectResponse. The purpose of these utilities is to ensure that redirects are safe.

Redirecting from a subscriber

Many times, our business logic dictates that we need to perform a redirect from a certain page to another if various conditions match. In these cases we can subscribe to the request event and simply change the response, essentially bypassing the normal process, which would have gone through all the layers of Drupal. However, before we see an example, let's talk about the Event Dispatcher for just a bit.

The central player in this system is the event_dispatcher service, which is an instance of the ContainerAwareEventDispatcher class. This service allows the dispatching of named events that take a payload in the form of an Event object, which wraps the data that needs to be passed around. Typically, when dispatching events, you'll create an Event subclass with some handy methods for accessing the data that needs to be passed around. Finally, instances of EventSubscriberInterface "listen" to events that have certain names and can alter the Event object that has been passed. Essentially, then, this system allows subscribers to change data before the business logic uses it for something. In this respect, it is a prime example of an extension point in Drupal. Finally, registering event subscribers is a matter of creating a service tagged with event_subscriber and that implements the interface I mentioned earlier.

Let's now take a look at an example event subscriber that listens to the kernel.request event and redirects to the home page if a user with a certain role tries to access our Hello World page. This will demonstrate both how to subscribe to events and how to perform a redirect. It will also show us how to use the current route match service to inspect the current route.

Let's create this subscriber by first writing the service definition for it:

hello_world.redirect_subscriber:
  class: \Drupal\hello_world\EventSubscriber\HelloWorldRedirectSubscriber
  arguments: ['@current_user']
  tags:
    - { name: event_subscriber }

As you can see, we have the regular service definition with one argument and with the event_subscriber tag. The dependency is actually the service that points to the current user (either logged in or anonymous) in the form of an AccountProxyInterface. This is a wrapper to the AccountInterface, which represents the actual current user. Also, when I say user, I mean an object that has certain data about the user and not the actual entity object with all the field data. It's the user session, basically. Certain things about the user are, however, accessible from the AccountInterface, such as the ID, name, roles, and email. I recommend that you check out the interface for more info. However, for our example, we will use it to check whether the user has the non_grata role, which will trigger the redirect I mentioned.

Next, let's look at the event subscriber class itself:

namespace Drupal\hello_world\EventSubscriber;
use Drupal\Core\Session\AccountProxyInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
/**
 * Subscribes to the Kernel Request event and redirects to the homepage
 * when the user has the "non_grata" role.
 */
class HelloWorldRedirectSubscriber implements EventSubscriberInterface {
  /**
   * @var \Drupal\Core\Session\AccountProxyInterface
   */
  protected $currentUser;
  /**
   * HelloWorldRedirectSubscriber constructor.
   *
   * @param \Drupal\Core\Session\AccountProxyInterface $currentUser
   */
  public function __construct(AccountProxyInterface $currentUser) {
    $this->currentUser = $currentUser;
  }
  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    $events['kernel.request'][] = ['onRequest', 0];
    return $events;
  }
  /**
   * Handler for the kernel request event.
   *
   * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
   */
  public function onRequest(GetResponseEvent $event) {
    $request = $event->getRequest();
    $path = $request->getPathInfo();
    if ($path !== '/hello') {
      return;
    }
    $roles = $this->currentUser->getRoles();
    if (in_array('non_grata', $roles)) {
      $event->setResponse(new RedirectResponse('/'));
    }
  }
}

As expected, we store the current user as a class property so that we can use it later on. Then, we implement the EventSubscriberInterface::getSubscribedEvents() method. This method needs to return a multidimensional array, which is basically a mapping between event names and the class methods to be called if that event is intercepted. This is how we actually register methods to listen to one event or another, and we can listen to multiple events in the same subscriber class if we want. It's typically a good idea to separate these, however, into different, more topical classes. The callback method name is inside an array whose second value represents the priority of this callback compared to others you or other modules may define. The higher the number, the higher the priority, which means the earlier in the process it will run. Do check the documentation on the interface itself for a good description of the ways you can subscribe to events.

In our example, we listen to the kernel.request event I mentioned in the previous chapter. This event is dispatched by Symfony's HttpKernel, passing an instance of GetResponseEvent, which basically wraps the Request object. The name of the Event class usually well describes the purpose of the event. In this case, it is looking for a Response object to deliver to the browser. If we inspect the class, we can see that it has a setResponse() method on it, which we can use to set the response. If a subscriber provides one, it stops the event propagation (none of the other listeners with a lower priority are given a chance) and the response is returned.

So, in our onRequest() callback method, we check the current path being requested, and if it is ours and the current user has the non_grata role, we set the RedirectResponse onto the event to redirect it to the home page. This will do the job we set out to do. If you go to the /hello page as a user with that role, you should be redirected to the home page. That being said, I don't like many aspects about this implementation. So, let's fix them.

First, we hardcoded the kernel.request event name (I did—I can't blame you for that). Any decent code that dispatches events will use a class constant to define the event name and the subscribers should also reference that constant. Symfony has the KernelEvents class just for that purpose. Check it out and see what other events are dispatched by the HttpKernel, as they are all referenced there.

So, instead of hardcoding the string, we can have this:

$events[KernelEvents::REQUEST][] = ['onRequest', 0];

Second, the way we do the path handling in the onRequest() method is all sorts of wrong. We are hardcoding the /hello path in this condition. What if we change the route path because our boss wants the path to be /greeting? I also don't like the way we passed the path to the RedirectResponse. The same thing applies (although in the case of the home page, not so much): what if the path we want to redirect to changes? Let's fix these problems using routes instead of paths. They are system-specific and are unlikely to change because of business requirements.

The problem is that we are unable to understand which route is being accessed from the Request object. Instead then, we can use the current_route_match service—a very popular one you'll use often—which gives us loads of info about the current route. So, let's inject that into our event subscriber. By now, you should know how to do this on your own (check the final code if you still have trouble). Once that is done, we can do this instead:

public function onRequest(GetResponseEvent $event) {
  $route_name = $this->currentRouteMatch->getRouteName();
  if ($route_name !== 'hello_world.hello') {
    return;
  }
  $roles = $this->currentUser->getRoles();
  if (in_array('non_grata', $roles)) {
    $url = Url::fromUri('internal:/');
    $event->setResponse(new LocalRedirectResponse($url->toString()));
  }
}

From the CurrentRouteMatch service, we can figure out the name of the current route, the entire route object, parameters from the URL, and other useful things. Do check out the class for more info on what you can do, as I guarantee that they will come in handy.

Instead of checking against the path name, we now check against the route name. So, if we change the path in the route definition, our code will still work. Then, instead of just adding the path to the RedirectResponse, we can build it first using the Url class we learned about in the previous section. Granted, in our example, it is probably overkill, but had we redirected it to a known route, we could have built it based on that, and our code would have been more robust. Additionally, using the Url class, we can also check other things, such as access, and its toString() method simply turns it into a string that can be used for the RedirectResponse. Finally, instead of the simple RedirectResponse, we are using the LocalRedirectResponse class instead as we are redirecting to a local (safe) path.

With this, we will get the same redirect, but in a much cleaner and more robust way. Of course, only after adjusting the use statements at the top by removing the one for the RedirectResponse and adding the following:

use Drupal\Core\Routing\CurrentRouteMatch; 
use Drupal\Core\Routing\LocalRedirectResponse; 
use Symfony\Component\HttpKernel\KernelEvents; 
use Drupal\Core\Url; 

Note

Again, for the sake of not overloading you with too much information, I omitted a very important aspect here: caching. So, our redirect works, but not very well. We will fix it when we learn about caching in Chapter 11, Caching.

Dispatching events

Since we have discussed how to subscribe to events in Drupal, we should also take a look at how we can dispatch our own events. After all, the Symfony Event Dispatcher component is one of the principal vectors of extensibility in Drupal.

To demonstrate this, we will create an event to be dispatched whenever our HelloWorldSalutation::getSalutation() method is called. The purpose is to inform other modules that this has happened and potentially allow them to alter the message that comes out of the configuration object—not really a solid use case, but good enough to demonstrate how we can dispatch events.

The first thing that we will need to do is to create an event class that will be dispatched. It can go into the root of our module's namespace:

namespace Drupal\hello_world; 
 
use Symfony\Component\EventDispatcher\Event; 
 
/** 
 * Event class to be dispatched from the HelloWorldSalutation service. 
 */ 
class SalutationEvent extends Event { 
 
  const EVENT = 'hello_world.salutation_event'; 
 
  /** 
   * The salutation message. 
   * 
   * @var string 
   */ 
  protected $message; 
 
  /** 
   * @return mixed 
   */ 
  public function getValue() { 
    return $this->message; 
  } 
 
  /** 
   * @param mixed $message 
   */ 
  public function setValue($message) { 
    $this->message = $message; 
  } 
}  

The main purpose of this event class is that an instance of it will be used to transport the value of our salutation message. This is why we created the $message property on the class and added the getter and setter methods. Moreover, we use it to define a constant for the actual name of the event that will be dispatched. Finally, the class extends from the base Event class that comes with the Event Dispatcher component as a standard practice. We could also use that class directly, but we would not have our data stored in it as we do now.

Next, it's time to inject the Event Dispatcher service into our HelloWorldSalutation service. We have already injected config.factory, so we just need to add a new argument to the service definition:

arguments: ['@config.factory', '@event_dispatcher'] 

Of course, we will also receive it in the constructor and store it as a class property:

/** 
 * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface 
 */ 
protected $eventDispatcher; 
 
/** 
 * HelloWorldSalutation constructor. 
 * 
 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory 
 * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher 
 */ 
public function __construct(ConfigFactoryInterface $config_factory, EventDispatcherInterface $eventDispatcher) { 
  $this->configFactory = $config_factory; 
  $this->eventDispatcher = $eventDispatcher; 
} 

We will also have the obligatory use statement for the EventDispatcherInterface at the top of the file:

use Symfony\Component\EventDispatcher\EventDispatcherInterface;  

Now, we can make use of the dispatcher. So, instead of the following code inside the getSalutation() method:

if ($salutation !== "" && $salutation) { 
  return $salutation; 
}  

We can have the following:

if ($salutation !== "" && $salutation) { 
  $event = new SalutationEvent(); 
  $event->setValue($salutation); 
  $event = $this->eventDispatcher->dispatch(SalutationEvent::EV ENT, $event); 
  return $event->getValue(); 
}

So, with the above, we decided that if we are to return a salutation message from the configuration object, we want to inform other modules and allow them to change it. We first create an instance of our Event class and feed it the relevant data (the message). Then, we dispatch the named event and pass the event object along with it. Finally, we get the data from that instance and return it.

Pretty simple, isn't it? What can subscribers do? It's very similar to what we saw regarding the example on redirects in the previous section. All a subscriber needs to do is listen for the SalutationEvent::EVENT event and do something based on that. The main thing that it can do is use the setValue() method on the received event object to change the salutation message. It can also use the stopPropagation() method from the base Event class to inform the Event Dispatcher to no longer trigger other listeners that have subscribed to this event.

Note

At the time of writing, Drupal is still using deprecated code from the Symfony Event Dispatcher, which in version 4.3 has changed the signature of the EventDispatcherInterface::dispatch() method, as well as having introduced a different base Event class. Until Drupal makes this change as well, we will continue to use the deprecated code.