Drupal 9 Module Development - Third Edition

3 (2 reviews total)
By Daniel Sipos
    Advance your knowledge in tech with a Packt subscription

  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Chapter 2: Creating Your First Module

About 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!

Publication date:
August 2020
Publisher
Packt
Pages
626
ISBN
9781800204621

 

Chapter 2: Creating Your First Module

Now that we have covered some of the introductory aspects of Drupal module development, it's time to dive right into the meat of what we are doing here—module creation.

Here are some of the important topics that we will cover in this chapter:

  • Creating a new Drupal 9 module – the files that are necessary to get started
  • Creating a route and controller
  • Creating and using a service
  • Creating a form
  • Creating a custom block
  • Working with links
  • Using the Event Dispatcher

Concretely, in this chapter, we will create a new custom module called Hello World. In this module, we will define a route that maps to a controller and that outputs this age-old programming message. So, this will be our first win.

Next, we will define a service that our Controller will use to pimp out our message. After all, we don't want the same message presented to the user all day long. This simple example, however, will illustrate what services are and how to interact with the service container in order to make use of them.

Then, we will create a form where an administrator will be able to override the message shown on our page. It will be stored in configuration, and we will alter our service to make use of that configuration. The key takeaway here will be the use of the Form API. However, we will also discuss how to store some basic configuration values and add dependencies to our existing services.

Finally, we want to become a bit more flexible. Why should users only be greeted on a specific page? We will create a custom block that can be placed anywhere on the site and will display the same message. Here, we will see how block plugins are defined and how they can expose their own configuration forms to be more flexible.

Although not strictly related to our Hello World example, we will also look at how to work with links programmatically in Drupal. This is a very common task that any Drupal developer needs to do very often. Moreover, we will also look at using the Event Dispatcher component and, more importantly, subscribing to events. We'll illustrate this with a fairly common example of when you'd need to do this—performing redirects from incoming requests.

By the end of this chapter, you should have the foundational knowledge necessary to build your own module from scratch. Moreover, you should be able to understand and implement some of the most commonly used techniques in Drupal module development.

 

Creating a module

Creating a simple Drupal 9 module is not difficult. You only need one file for it to be recognized by the core installation and to be able to enable it. In this state, it won't do much, but it will be installable. Let's first take a look at how to do this, and then we will progressively add meat to it in order to achieve the goals set out at the beginning of the chapter.

Modules go inside the /modules folder of the Drupal application. Inside the /modules folder, there can be a /contrib folder, which stores contributed modules, and a /custom folder, where we put the modules we write custom for the specific application. And that is where we will place our custom module, called Hello World.

We will start by creating a folder called hello_world. This will also be the module's machine name used in many other places. Inside, we will need to create an info file that describes our module. This file is named hello_world.info.yml. This naming structure is important—first, the module name, then info, followed by the .yml extension. You will hear this file often referred to as the module's info file (due to it having had the .info extension in past versions of Drupal).

Inside this file, we will need to add some minimal information that describes our module. We will go with something like this:

name: Hello World
description: Hello World module
type: module
core_version_requirement: ^9
package: Custom

Some of this is self-explanatory, but let's see what these lines mean:

  • The first two represent the human-readable name and description of the module.
  • The type key means that this is a module info file rather than a theme.
  • The core_version_requirement key specifies that this module works with version 9 of Drupal, and it won't be installable on previous or future versions.
  • Finally, we place this in a generic Custom package so that it gets categorized in this group on the modules' administration screen.

That is pretty much it. The module can now be enabled either through the UI at /admin/modules or via Drush using the drush en hello_world command.

Note

Before Drupal 8.7.7, the way to indicate which version of Drupal the module was compatible with was through the core key, and it would allow you to specify only the major version (7.x, 8.x, and so on). Using the new core_version_requirement key, we can semantically specify which version of Drupal the module works with. For example, this would indicate the module is compatible with Drupal 8 as well: ^8.8 || ^9.

Before we move on, let's see what other options you can add (and probably will need to add at some point or another) to the info file:

Module dependencies: If your module depends on other modules, you can specify this in its info file like so:

dependencies:
  - drupal:views
  - ctools:ctools

The dependencies should be named in the project:module format, where project is the project name as it appears in the URL of the project on Drupal.org and module is the machine name of the module.

Configuration: If your module has a general configuration form that centralizes the configuration options of the module, you can specify the route of that form in the info file. Doing so will add a link to that form on the admin/modules UI page where modules are being installed:

configure: module_name.configuration_route_name

The module as it stands doesn't do much. In fact, it does nothing. However, do pat yourself on the back, as you have created your first Drupal 9 module. Before we move on to the interesting stuff we planned out, let's implement our first hook responsible for providing some helpful information about our module.

Your first hook implementation

As we hinted at in the first chapter, when Drupal encounters an event for which there is a hook (and there are hundreds of such events), it will look through all of the modules for matching hook implementations. Now, how does it find the matching implementations? It looks for the functions that are named in the module_name_hook_name format, where hook_name is replaced by the name of the hook being implemented and module_name is the module machine name. The name of a hook is whatever comes after hook_. We will see an example next when we implement hook_help(). However, once it finds the implementations, it will then execute each of them, one after another. Once all hook implementations have been executed, Drupal will continue its processing.

Hook implementations typically go inside a .module file, so let's create one in our module folder called hello_world.module and place an opening PHP tag at the top. Then, we can have the following hook_help() implementation inside (and typically all other hook implementations):

use Drupal\Core\Routing\RouteMatchInterface;
/**
 * Implements hook_help().
 */
function hello_world_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'help.page.hello_world':
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('This is an example module.') . '</p>';
      return $output;
    default:
  }
}

As you can see, the name of the function respects the previously mentioned format—module_name_hook_name—because we are implementing hook_help. So, we replaced hook with the module name and hook_name with help. Moreover, this particular hook takes two parameters that we can use inside it; though, in our case, we only use one, that is, the route name.

The purpose of this hook is to provide Drupal with some help text about what this module does. You won't always implement this hook, but it's good to be aware of it. The way it works is that each new module receives its own route inside the main module, where users can browse this info—ours is help.page.hello_world. So, in this implementation, we will tell Drupal (and, more specifically, the core Help module) the following: if a user is looking at our module's help route (page), show the info contained in the $output variable. And that's pretty much it.

According to the Drupal coding standards, the DocBlock message above the hook implementation needs to stay short and concise, as in the preceding example. We do not generally document anything further for Drupal core hooks or popular contrib module hooks because they should be documented elsewhere. If, however, you are implementing a custom hook defined in one of your modules, it›s okay to add a second paragraph describing what it does.

Users can now reach this page from the module administration page by clicking on the Help link for each individual module that has this hook implemented. Do remember to clear the cache first, though. Easy, right?

Figure 2.1: Hello World example module

Figure 2.1: Hello World example module

Even though we are not really providing any useful info through this hook, implementing it helped us understand how hooks work and what the naming convention is for using them. Additionally, we saw an example of a traditional (procedural) Drupal extension point that module developers can use. In doing so, we literally extended the capability of the Help module by allowing it to give more info to users.

Before we move on, let's quickly add a file comment to ensure we respect the Drupal coding standards. So, we add the following to the top of the .module file:

/**
 * @file
 * Hello World module file.
 */

Note

In order to keep the code examples on the pages of the book concise, going forward I will skip certain formatting required for respecting the Drupal coding standards. In the GitHub repository, however, all the code should be fully correct.

Now, let's move on to creating something of our own.

Route and controller

The first real piece of functionality we set out to create was a simple Drupal page that outputs the age-old Hello World string. To do this, we will need two things—a route and a controller. So, let's start with the first one.

The route

Inside our module, we will need to create our routing file, which will hold all our statically defined routes. The name of this file will be hello_world.routing.yml. By now, I assume that you understand what the deal is with the file naming conventions in a Drupal module. However, in any case, this is another YAML file in which we will need to put YAML-formatted data:

hello_world.hello:
  path: '/hello'
  defaults:
    _controller:  Drupal\hello_world\Controller\HelloWorldController::helloWorld
    _title: 'Our first route'
  requirements:
    _permission: 'access content'

This is our first route definition. It starts with the route name (hello_world.hello), followed by all the necessary info about it underneath, in a YAML-formatted multidimensional array. The standard practice is to have the route name start with the module name it is in, followed by route qualifiers as needed.

So, what does the route definition contain? There can be many options here, but for now, we will stick with the simple ones that serve our purpose.

Note

For more info about all route configuration options, visit the relevant documentation page at https://www.drupal.org/docs/8/api/routing-system/structure-of-routes. It is a good resource to keep on hand. Do note that Drupal documentation resources typically reference Drupal 8, but they should be generic enough or include notes about Drupal 9 as well. Also, most of the time, the information is relevant for both.

First, we have a path key, which indicates the path we want this route to work on. Then, we have a defaults section, which usually contains info relevant to the handlers responsible for delivering something when this route is accessed. In our case, we set the controller and method responsible for delivering the page, as well as its title. Finally, we have a requirements section, which usually has to do with conditions that need to be met for this route to be accessible (or be hit)—things such as permissions and format. In our case, we will require users to have the access content permission, which most visitors will have. Don't worry; we will cover more about access in Chapter 10, Access Control.

That is all we need for our first route definition. Now, we will need to create the Controller that maps to it and can deliver something to the user.

Before we do that, let's look at an example of a very common routing requirement you will most likely have to use really soon. We don't need this for the functionality we're building in this chapter, so I won't include it in the final code. However, it's important that you know how this works.

Route variables

A very common requirement is to have a variable route parameter (or more) that gets used by the code that maps to the route, for example, the ID or path alias of the page you want to show. These parameters can be added by wrapping a certain path element in curly braces, like so:

path: '/hello/{param}' 

Here, {param} will map to a $param variable that gets passed as an argument to the controller or handler responsible for this route. So, if the user goes to the hello/jack path, the $param variable will have the jack value and the controller can use that.

Additionally, Drupal comes with parameter converters that transform the parameter into something more meaningful. For example, an entity can be autoloaded and passed to the Controller directly instead of an ID. Also, if no entity is found, the route acts as a 404, saving us a good few lines of code. To achieve this, we will also need to describe the parameter so that Drupal knows how to autoload it. We can do so by adding a route option for that parameter:

options:
   parameters:
     param:
       type: entity:node

So, we have now mapped the {param} parameter to the node entity type. Hence, if the user goes to hello/1, the node with the ID of 1 will be loaded (if it exists).

We can do one better. If, instead of {param}, we name the parameter {node} (the machine name of the entity type), we can avoid having to write the parameters option in the route completely. Drupal will figure out that it is an entity and will try to load that node by itself. Neat, no?

So, keep these things in mind the next time you need to write dynamic routes.

Namespaces

Before moving on with the Controller we set out to write, let's break down the namespace situation in Drupal and how the folder structure needs to be inside a module.

Drupal uses the PSR-4 namespace autoloading standard. In effect, this means that the namespace of all Drupal core and module classes starts with \Drupal. For modules, the base namespace is \Drupal\module_name, where module_name is the machine name of the module. This then maps to the /src folder found inside the module directory (for main integration files). For PHPUnit tests, we have a different namespace, as we will see in Chapter 17, Automated Testing.

So essentially, we will need a /src folder inside our module to place all of our classes that need to be autoloaded. So, we can go ahead and create it.

The Controller

Now that we have found where we have to place our Controller, let's begin by creating a Controller folder inside our module›s /src folder. Although not mandatory, this is a standard practice for Controller placement. Inside this folder, we can have our first Controller class file: HelloWorldController.php.

Inside the file, we again have something simple (after the opening PHP tags):

namespace Drupal\hello_world\Controller;
use Drupal\Core\Controller\ControllerBase;
/**
 * Controller for the salutation message.
 */
class HelloWorldController extends ControllerBase {
  /**
   * Hello World.
   *
   * @return array
   *   Our message.
   */
  public function helloWorld() {
    return [
      '#markup' => $this->t('Hello World'),
    ];
  }
}

As expected, we start with the namespace declaration. If you read the previous section, the namespace choice will make sense. Then, we have our Controller class, which extends ControllerBase, which happens to provide some helper tools (such as the StringTranslationTrait, which I will explain later in Chapter 13, Internationalization and Languages). If you recall our route definition, we referenced a helloWorld method on this Controller.

If you've worked with previous versions of Drupal, this array (called a render array) will be familiar. Otherwise, what you need to know right now is that we are returning simple markup with the Hello World text wrapped in the translation service I hinted at in the previous paragraph. After the Controller returns this array, there will be an EventSubscriber that takes this array, runs it through the Drupal theme layer, and returns the HTML page as a response. The actual content returned in the Controller will be wrapped in the Main page content block, which is usually placed in the main content region of the theme.

Now, our simple Controller is done. If we clear the cache and go to /hello, we should encounter a new page that outputs the Our first route title and the Hello World content. Success!

Note

You can clear the cache by going to Admin -> Configuration -> Development -> Performance or by running the drush cache-rebuild command.

Figure 2.2: Controller interface

Figure 2.2: Controller interface

Services

Why don't I like this approach?

Even if for the moment not much is happening in it, I don't want the Controller making decisions on how to greet my users. First of all, because Controllers need to stay lean. I want my users to be greeted a bit more dynamically, depending on the time of day, and that will increase the complexity. Second of all, maybe I will want this greeting to be done elsewhere as well, and there is no way I am copy-pasting this logic somewhere else, nor am I going to misuse the Controller just to be able to call that method. The solution? We delegate the logic of constructing the greeting to a service and use that service in our Controller to output the greeting.

What is a service?

A service is an object that gets instantiated by a Service Container and is used to handle operations in a reusable way, for example, performing calculations and interacting with the database, an external API, or any number of things. Moreover, it can take dependencies (other services) and use them to help out. Services are a core part of the dependency injection (DI) principle that is commonly used in modern PHP applications.

If you don't have any experience with these concepts, an important thing to note is also that they are globally registered with the service container and are (usually) instantiated only once per request. This means that altering them after you requested them from the container means that they stay altered even if you request them again. In essence, they are singletons. So, you should write your services in such a way that they stay immutable, and most of the data they need to process is either from a dependency or passed in from the client that uses it (and does not affect it). Although this is the case for most services, there are some that work differently, in that they get re-created with each request. But these examples are rare, and we should not overload the job at hand by talking about them here.

Note

Many Drupal core service definitions can be found inside the core.services.yml file located in the root /core folder. So, if you are ever looking for service names to use, your best bet is to look there. Additionally, core modules also have service definitions inside their respective *.services.yml files. So, make sure that you also check there.

The HelloWorldSalutation service

Now that we have a general idea as to what a service is, let's create one to see all this in practice.

As I mentioned earlier, I want my greetings to be more dynamic, that is, I want the salutation to depend on the time of day. So, we will create a (HelloWorldSalutation) class that is responsible for doing that and place it in the /src folder (our module's namespace root) in a file naturally called HelloWorldSalutation.php:

namespace Drupal\hello_world;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
 * Prepares the salutation to the world.
 */
class HelloWorldSalutation {
  use StringTranslationTrait;
  /**
   * Returns the salutation
   */
  public function getSalutation() {
    $time = new \DateTime();
    if ((int) $time->format('G') >= 00 && (int) $time->format('G') < 12) {
      return $this->t('Good morning world');
    }
    if ((int) $time->format('G') >= 12 && (int) $time->format('G') < 18) {
      return $this->t('Good afternoon world');
    }
    if ((int) $time->format('G') >= 18) {
      return $this->t('Good evening world');
    }
  }
}

Note

From now on, I will not always mention the file name that a particular class goes into. So, you can safely assume one file per class, named after the class itself.

By now, I assume that the namespace business is clear, so I won't explain it again. Let's see what else we did here. First, we used the StringTranslationTrait in order to expose the translation function. Second, we created a rudimentary method that returns a different greeting depending on the time of day. This could probably have been done better, but for the purposes of this example, it works just fine.

Note

In this example I used the native PHP function time() to get the current time, and that's OK. But you should know that Drupal has its very own Drupal\Component\Datetime\Time service that we can use to get the current time. It also has additional methods for requesting time-specific information, so make sure you check it out and use it when appropriate.

Now that we have our class, it's time to define it as a service. We don't want to be going new HelloWorldSalutation() all over our code base, but instead, register it with the Service Container and use it from there as a dependency. How do we do that?

First, we will need, yet again, a YAML file: hello_world.services.yml. This file starts with the services key, under which will be all the service definitions of our module. So, our file will look like this (for now):

services:
  hello_world.salutation:
    class: Drupal\hello_world\HelloWorldSalutation

This is the simplest possible service definition you can have. You give it a name (hello_world.salutation) and map it to a class to be instantiated. It is standard practice to have the service name start with your module name.

Once we clear the cache, the service will get registered with the Service Container and will be available to use.

Note

If there is any reason to believe that you will have more than one salutation service, you should create an interface that this class can implement. This way, you'll be able to always type hint that interface instead of the class and make the implementations swappable. In fact, having interfaces is best practice.

Tagged services

Service definitions can also be tagged in order to inform the container if they serve a specific purpose. Typically, these are picked up by a collector service that uses them for a given subsystem. As an example, if we wanted to tag the hello_world.salutation service, it would look something this:

hello_world.salutation:
  class: Drupal\hello_world\HelloWorldSalutation
  tags:
    - {name: tag_name}

Tags can also get a priority, as we will see in some examples later in this book.

Before we go and use our service in the Controller we created, let's take a breather and run through the ways you can make use of services once they are registered.

 

Using services in Drupal

There are essentially two ways of using services—statically and injected. The first is done by a static call to the service container, whereas the second uses dependency injection to pass the object through the constructor (or in some rare cases, a setter method). However, let's check out how, why, and what the real difference is.

Statically, you would use the global \Drupal class to instantiate a service:

$service = \Drupal::service('hello_world.salutation');

This is how we use services in .module files and classes that are not exposed to the service container and into which we cannot inject. Instances of the latter are rare, though; most of the time, we use the static calls only from within static contexts.

A few popular services also have shorthand methods on the \Drupal class; for example, \Drupal::entityTypeManager(). I recommend that you inspect the \Drupal class and take a look at the ones with shorthand methods available.

It is not good to use the static method of service instantiation inside a Controller, service, plugin, or any other class where dependency injection is an option. The reason is that it defeats much of the purpose of using a service, as it couples the two, making it a nightmare to test. Inside hook implementations and other Drupal-specific procedural code, on the other hand, we have no choice, and it is normal to do so.

Moreover, just because a piece of code is inside a .module file, it doesn't mean that it should be there. In general, these modules should only contain things such as hook implementations or any other implementations that require a certain naming convention to be respected. They should also be lean and have their work delegated to services.

The proper way to use services is to inject them where needed. Admittedly, this approach is a bit more time-consuming but, as you progress, it will become second nature. Also, since there are a few different ways to inject dependencies (based on the receiver), we will not cover them here. Instead, we will see how they work throughout this book, at the right time. We will take a look at a very important example right now in the next section.

Injecting the service into our Controller

Let's now continue with our module and take a look at how to inject the newly created service into our Controller.

We will need to add some code to the Controller (typically at the beginning of the class so that we can immediately identify the presence of this code when looking at it):

/**
 * @var \Drupal\hello_world\HelloWorldSalutation
 */
protected $salutation;
/**
 * HelloWorldController constructor.
 *
 * @param \Drupal\hello_world\HelloWorldSalutation $salutation
 */
public function __construct(HelloWorldSalutation $salutation) {
  $this->salutation = $salutation;
}
/**
 * {@inheritdoc}
 */
public static function create(ContainerInterface $container) {
  return new static(
    $container->get('hello_world.salutation')
  );
}

In addition to this, ensure that you include the relevant use statements at the top of the file:

use Drupal\hello_world\HelloWorldSalutation;
use Symfony\Component\DependencyInjection\ContainerInterface;

So, what is going on here? First, we give the Controller a constructor method, which takes our service as an argument and stores it as a property. For me, this is usually the very first method in the class. But how does this constructor get its argument? It gets it via the create() method, which receives the Service Container as a parameter and is free to choose the service(s) needed by the Controller constructor. This is usually my second method in a class. I prefer this order because it's very easy to check whether these methods are present. Also, their presence is important, especially when inheriting and observing what the parent is injecting.

OK, but how does this injection business work in reality?

In a nutshell, after the route is found and the responsible Controller is resolved, a check is made to see whether the latter implements ContainerInjectionInterface. Our Controller does so via its parent ControllerBase. If it does, the Controller gets instantiated via the create() method and the container is passed to it. From there, it is responsible for creating a new static version of itself with the required services from the container—not that complicated, really!

The create() method is a staple practice in the Drupal dependency injection pattern, so you will see it quite a lot. However, one thing to keep in mind is that you should never pass the entire container to the class you instantiate with it because you are no longer doing dependency injection then.

A note about ControllerBase, which we are extending—it is a standard practice to extend it, but not mandatory as controllers are nothing more than simple callables. It provides some nice traits, implements interfaces that are required, and immediately shows what the purpose of our class is. However, from the point of view of dependency injection, I advise against using the helper methods that return services (for example, entityTypeManager()). They, unfortunately, load services statically, which is not the best practice in this case. You should instead inject them yourself, as we did just now.

OK, let's turn back to our example. Now that we have the service injected, we can use it to render the dynamic salutation:

return [
  '#markup' => $this->salutation->getSalutation(),
];

There we have it. Now our greeting is dependent on the time of day and our Controller is dependent on our salutation service.

One thing I would like to specify about our example is that I disregarded caching for the sake of simplicity. With caching turned on, the page would be cached and served with potentially the wrong salutation. However, in Chapter 11, Caching, we will cover all these intricacies, so there is no point in complicating our example now.

Invoked Controllers

Now that we know what routes, Controllers, and services are, I'd also like to quickly note that Controllers can be defined as services and invoked by the routing system. In other words, just as we defined our hello_world.salutation service, we could define another one that would act as a Controller and reference that service ID in the routing file instead of the fully qualified class name. Then, in order for Drupal to know which method inside the service to call when a user accesses the route, we would need to implement the magic __invoke method inside the service. The rest would work pretty much in the same way.

This capability was introduced in Drupal 8.7 and is typical to the Action-Domain-Responder architectural pattern. We won't use it going forward but it's good to know that it's available.

 

The Form API

Our page displays a greeting dynamically, depending on the time of day. However, we now want an administrator to specify what the greeting should actually be, in other words, to override the default behavior of our salutation if they so choose.

The ingredients for achieving this will be as follows:

  • A route (a new page) that displays a form where the administrator can set the greeting
  • A configuration object that will store the greeting

In building this functionality, we will also take a look at how to add a dependency to our existing service. So, let's get started with our new route, which naturally goes inside the hello_world.routing.yml file we already created:

hello_world.greeting_form:
  path: '/admin/config/salutation-configuration'
  defaults:
    _form: Drupal\hello_world\Form\SalutationConfigurationForm
    _title: 'Salutation configuration'
  requirements:
    _permission: 'administer site configuration'

Most of this route definition is the same as we saw earlier. There is one change, though, in that it maps to a form instead of a Controller. This means that the entire page is a form page. Also, since the path is within the administration space, it will use the administration theme of the site. What is left to do now is to create our form class inside the /Form folder of our namespace (a standard practice directory for storing forms, but not mandatory).

Due to the power of inheritance, our form is actually very simple. However, I will explain what goes on in the background and guide you on your path to building more complex forms. So, here we have our form:

namespace Drupal\hello_world\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
 * Configuration form definition for the salutation message.
 */
class SalutationConfigurationForm extends ConfigFormBase {
  /**
   * {@inheritdoc}
   */
  protected function getEditableConfigNames() {
    return ['hello_world.custom_salutation'];
  }
  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'salutation_configuration_form';
  }
  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $config = $this->config('hello_world.custom_salutation');
    $form['salutation'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('Salutation'),
      '#description' => $this->t('Please provide the salutation you want to use.'),
      '#default_value' => $config->get('salutation'),
    );
    return parent::buildForm($form, $form_state);
  }
  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->config('hello_world.custom_salutation')
      ->set('salutation', $form_state->getValue('salutation'))
      ->save();
    parent::submitForm($form, $form_state);
  }
}

Clearing the cache and navigating to admin/config/salutation-configuration will present you with your simple configuration form via which you can save a custom salutation message:

Figure 2.3: Salutation configuration form

Figure 2.3: Salutation configuration form

Later on, we will make use of that value. However, first, let's talk a bit about forms in general, and then this form in particular.

A form in Drupal is represented by a class that implements FormInterface. Typically, we either extend from FormBase or ConfigFormBase, depending on what its purpose is. In this case, we created a configuration form, so we extended from the latter class.

There are four main methods that come into play in this interface:

  • getFormId(): Returns a unique, machine-readable name for the form.
  • buildForm(): Returns the form definition (an array of form element definitions and some extra metadata, as needed).
  • validateForm(): The handler that gets called to validate the form submission. It receives the form definition and a FormStateInterface object that contains, among others, the submitted values. You can flag invalid values on their respective form elements, which means that the form is not submitted but refreshed (with the offending elements highlighted).
  • submitForm(): The handler that gets called when the form is submitted (if validation has passed without errors). It receives the same arguments as validateForm(). You can perform operations such as saving the submitted values or triggering some other kind of flow.

Defining a form, in a nutshell, means creating an array of form element definitions. The resulting form is very similar to the render array we mentioned earlier. When creating your forms, you have a large number of form element types to use. A complete reference of what they are and what their options are (their definition specificities) can be found on the Drupal Form API reference page (https://api.drupal.org/api/drupal/elements/9.0.x).

From a dependency injection point of view, forms can receive arguments from the service container in the same way that we injected the salutation service into our Controller. As a matter of fact, ConfigFormBase, which we are extending in our example, injects the config.factory service because it needs to use it for reading and storing configuration values. This is why we extend from that form. Drupal is full of these helpful classes that we can extend and that provide a bunch of useful boilerplate code that is very commonly used across the Drupal ecosystem.

If the form you are building is not storing or working with configuration, you will typically extend from FormBase, which provides some static methods and traits and also implements some interfaces. The same word of caution goes for its helper service methods as for the ones of ControllerBase: if you need services, you should always inject them.

Let's turn to our form class and dissect it a bit now that we know a thing or two about forms.

We have the getFormId() method. Check. We also have buildForm() and submitForm(), but not validateForm(). The latter is not mandatory, and we don't actually need it for our example, but if we did, we could have something like this:

/**
 * {@inheritdoc}
 */
public function validateForm(array &$form, FormStateInterface $form_state) {
  $salutation = $form_state->getValue('salutation');
  if (strlen($salutation) > 20) {
    $form_state->setErrorByName('salutation', $this->t('This salutation is too long'));
  }
}

In this validation handler, we basically check whether the submitted value for the salutation element is longer than 20 characters. If so, we set an error on that element (to turn it red usually) and specify an error message on the form state specific to this error. The form will then be refreshed and the error will be presented, and the submit handler, in this case, will not be called.

For the purposes of our example, this is, however, not necessary, so I will not include it in the final code.

Note

Form validation error messages, by default, are printed at the top of the page. However, with the core Inline Form Errors module, we can have the form errors printed right beneath the actual elements. This is much better for accessibility, as well as for clarity when dealing with large forms. Note that the standard Drupal 9 installation doesn't have this module enabled, so you'll have to enable it yourself if you want to use it.

If we turn back to our form class, we also see a strange getEditableConfigNames() method. This is required by the ConfigFormBaseTrait, which is used in the ConfigFormBase class that we are extending. It needs to return an array of configuration object names that this form intends to edit. This is because there are two ways of loading configuration objects: for editing and for reading (immutable). With this method, we inform it that we want to edit that configuration item.

As we see on the first line of buildForm(), we are using the config() method of the previously mentioned trait to load up our editable configuration object from the Drupal configuration factory. This is to check the value that is currently stored in it. Then, we define our form elements (in our case, one—a simple text field). For #default_value (the value present in the element when the user goes to the form), we put whatever is in the configuration object. The rest of the element options are self-explanatory and pretty standard across all element types. Consult the Form API reference to see what other options are available and for which element types. Finally, at the end of the method, we also call the parent method because that provides the form's submit button, which for our purposes is enough.

The last method we need is the submit handler, which basically loads up the editable configuration object, puts the submitted value in it, and then saves it. Finally, it also calls the parent method, which then simply sends a success message to the user on the screen using the Messenger service—a standard way of showing the user a success or error message.

That is pretty much it; this will work just fine.

From the point of view of configuration, we used ConfigFormBase to make our lives easier and combine the form aspect with that of the configuration storage. In a later chapter, we will talk more about the different types of storage and also cover how to work with configuration objects. So, no worries if you are left a bit unclear about how configuration works.

Altering forms

Before going ahead with our proposed functionality, I would like to open a parenthesis and discuss forms in a bit more detail. An important thing that you will do as a module developer is alter forms defined by other modules or Drupal core. So, it behooves us to talk about it early on and what better moment than now, when defining the form itself is still fresh in our minds.

Obviously, the form we just created belongs to us and we can change it however we want. However, many forms out there have been defined by other modules and there will be just as many times that you will want to make changes to them. Drupal provides us with a very flexible, albeit still procedural, way of doing so—a suite of alter hooks; but what are alter hooks?

The first thing we did in this chapter was implement hook_help(). That is an example of an invoked hook by which a caller (Drupal core or any module) asks other modules to provide input. This input is then aggregated in some way and made use of. The other type of hooks we have in Drupal are the alter hooks, which are used to allow other modules to make changes to an array or an object before that array or object is used for whatever it is used for. So, in the case of forms, there are some alter hooks that allow modules to make changes to the form definition before it's processed for rendering.

You may be wondering why I am saying that to make changes to a form, we have more than one alter hook. Let me explain by giving an example of how other modules could alter the form we just defined (will not be included in our code base):

/**
 * Implements hook_form_alter().
 */
function my_module_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
  if ($form_id === 'salutation_configuration_form') {
    // Perform alterations. 
  }
}

In the code above, we implement the generic hook_form_alter(), which gets fired for all forms when being built, and we do so inside a module called my_module. The first two arguments are the form and form state (the same as we saw in the form definition), the former being passed by reference. This is the typical alter concept—we make changes to an existing variable and don't return anything. The third parameter is the form ID, the one we defined in the getFormId() method of our form class. We check to ensure that the form is correct and then we can make alterations to the form.

This is, however, almost always the wrong approach, because the hook is fired for all forms indiscriminately. Even if we don't actually do anything for most of them, it's still a useless function call, not to mention that if we want to alter 10 forms in our module, there will be a lot of if conditionals in there—the price we pay for procedural functions. Instead, though, we can do this:

/**
 * Implements hook_form_FORM_ID_alter().
 */
function my_module_form_salutation_configuration_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
  // Perform alterations. 
}

Here, we are implementing hook_form_FORM_ID_alter(), which is a dynamic alter hook in that its name contains the actual ID of the form we want to alter. So, with this approach, we ensure that this function is called only when it's time to alter OUR form. The other benefit is that if we need to alter another form, we can implement the same kind of hook for that and have our logic neatly separated.

Custom submit handlers

So, up to now, we have seen how other modules can make changes to our form. That means adding new form elements, changing existing ones, and so on. But what about our validation and submit handlers (those methods that get called when the form is submitted). How can those be altered?

Typically, for the forms defined as we did, it's pretty simple. Once we alter the form and inspect the $form array, we can find a #submit key, which is an array that has one item: ::submitForm. This is simply the submitForm() method on the form class. So, what we can do is either remove this item and add our own function, or simply add another item to that array:

/**
 * Implements hook_form_FORM_ID_alter().
 */
function my_module_form_salutation_configuration_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
  // Perform alterations.
  $form['#submit'][] = 'my_module_salutation_configuration_form_submit';
}

And the callback we added to the #submit array above can look like this:

/** 
 * Custom submit handler for the form_salutation_configuration form. 
 * 
 * @param $form 
 * @param \Drupal\Core\Form\FormStateInterface $form_state 
 */ 
function my_module_salutation_configuration_form_submit(&$form, \Drupal\Core\Form\FormStateInterface $form_state) { 
  // Do something when the form is submitted. 
} 

So, the cool thing is that you can choose to tack on your own callback or replace the existing one. Keep in mind that the order they are located in that array is the order in which they get executed. So, you can also change the order if you want.

There is another case though. If the submit button on the form has a #submit property specifying its own handler, the default form #submit handlers we saw just now won't fire anymore. This was not the case with our form. So, in that situation, you will need to add your own handler to that form's submit element array instead.

Finally, when it comes to the validation handler, it works exactly the same as with the submit handler, but it all happens under the #validate array key.

Feel free to experiment with altering existing forms and inspect the variables they receive as arguments.

Rendering forms

Staying on forms for just a bit longer, let's quickly learn how to render forms programmatically. We have already seen how to map a form to a route definition so that the page being built contains the form when accessing the route path. However, there are times when we need to render a form programmatically, either inside a Controller or a block, or wherever we want. We can do this using the FormBuilder service.

The form builder can be injected using the form_builder service key or used statically via the shorthand:

$builder = \Drupal::formBuilder(); 

Once we have it, we can build a form, like so:

$form = $builder->getForm('Drupal\hello_world\Form\SalutationConfigurationForm'); 

In the code above, $form will be a render array of the form that we can return, for example, inside a Controller. We'll talk more about render arrays a bit later on, and you›ll understand how they get turned into actual form markup. However, for now, this is all you need to know about rendering forms programmatically—you get the form builder and request from it the form using the fully qualified name of the form class.

With this, we can close the parenthesis on forms.

Service dependencies

In the previous section, we created a form that allows administrators to set up a custom salutation message to be shown on the page. This message was stored in a configuration object that we can now load in our HelloWorldSalutation service. So, let›s do just that with a two-step process.

First, we will need to alter our service definition to give our service an argument—the configuration factory (the service responsible for loading config objects). This is how our service definition should look now:

hello_world.salutation:
  class: Drupal\hello_world\HelloWorldSalutation
  arguments: ['@config.factory']

The addition is the arguments key, which is an array of service names proceeded by @. In this case, config.factory is the responsible service name, which, if we check in the core.services.yml file, we can note that it maps to the Drupal\Core\Config\ConfigFactory class.

So, with this change, the HelloWorldSalutation class will be passed an instance of ConfigFactory. All we need to do now is adjust our class to actually receive it:

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

There's nothing too complicated going on here. We added a constructor and set the config factory service on a property. We can now use it to load our configuration object that we saved in the form. However, before we do that, we should also use the ConfigFactoryInterface class at the top of the file:

use Drupal\Core\Config\ConfigFactoryInterface;  

Now, at the top of the getSalutation() method, we can add the following bit:

$config = $this->configFactory->get('hello_world.custom_salutation');
$salutation = $config->get('salutation');
if ($salutation !== "" && $salutation) {
  return $salutation;
}

With this addition, we are loading the configuration object we saved in the form, and from it, we request the salutation key, where, if you remember, we stored our message. If there is a value in there, we will return it. Otherwise, the code will continue, and our previous logic of time-based greeting will apply.

So now, if we reload our initial page, the message we saved through the form should show up. If we then return to the form and remove the message, this page should default back to the original dynamic greeting. Neat, right?

Let's now take a look at how we can create a custom block that we can place anywhere we like and that will output the same thing as our page.

 

Blocks

Blocks, since Drupal 8, are plugins. However, the blocks you create in the UI are content entities and the placement of blocks (of both types) in the block layout are configuration entities. So, the block system is a good example of how entities and plugins work hand in hand in Drupal. We will talk in more detail about plugin types and entities later in the book.

So, how do we create a custom block plugin? All we need is one class, placed in the right namespace—Drupal\module_name\Plugin\Block. In this case (with plugins), the folder naming is important. The plugin discoverability is dependent on the plugin type itself, and this one has the Plugin\Block namespace bit in it. But enough talk; let's create a simple block that just renders the same as our Controller did previously, and I will explain things along the way.

Our first block plugin

So, this is our plugin class—HelloWorldSalutationBlock—which does just that:

namespace Drupal\hello_world\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\hello_world\HelloWorldSalutation;
/**
 * Hello World Salutation block.
 *
 * @Block(
 *  id = "hello_world_salutation_block",
 *  admin_label = @Translation("Hello world salutation"),
 * )
 */
class HelloWorldSalutationBlock extends BlockBase implements ContainerFactoryPluginInterface {
  /**
   * The salutation service.
   *
   * @var \Drupal\hello_world\HelloWorldSalutation
   */
  protected $salutation;
  /**
   * Constructs a HelloWorldSalutationBlock.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, HelloWorldSalutation $salutation) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->salutation = $salutation;
  }
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('hello_world.salutation')
    );
  }
  /**
   * {@inheritdoc}
   */
  public function build() {
    return [
      '#markup' => $this->salutation->getSalutation(),
    ];
  }
}

Before even going through the explanation, you should know that clearing the cache and placing this block through the block management UI will do what we wanted. However, let's understand what is going on here first.

Perhaps the strangest thing you'll note is the DocBlock comment at the top of the class. This is called an annotation and denotes that this class is a Block plugin. As I mentioned in the first chapter, annotations are the most common discovery mechanisms for plugins in Drupal core. In this case, the plugin definition we need is made up of an ID and an administration label.

Note

Properly defined plugin types have an AnnotationInterface implementation, which describes the properties that can or should be used in the annotation. So, if you are unsure as to what needs to be there, look for this class for that specific plugin type.

Then, we see that our class extends BlockBase and also implements the ContainerFactoryPluginInterface. The former, similar to the ControllerBase and FormBase we saw earlier, provides a number of helpful things a block plugin needs. However, we cannot really get around extending this class because block plugins are quite complex, working with things such as context and configuration. So, ensure that you always extend this class. The latter is, however, optional. That interface makes this block plugin container-aware, that is, at the moment of instantiation, it uses the create() method to build itself using the service container for dependencies. And sure enough, we have our create() method as well.

Before moving on to the actual block building, we need to talk a bit about dependency injection in plugins. As you can see, the signature of this create() method is different to the one we saw in the Controller. This is also why we are using a different container-aware interface. The reason is that plugins are constructed with a few extra parameters: $configuration, $plugin_id, and $plugin_definition. The first contains any configuration values that are needed by the plugin, the second is the ID set in the plugin annotation (or other discovery mechanism), and the third is an array that contains the metadata of this plugin (including all the info found in the annotation). However, apart from this, it's business as usual when it comes to dependency injection. If a plugin type base class doesn't implement this interface, you can do so yourself directly in your plugin. This works with most plugins, save for a few exceptions that cannot be made container-aware, but this happens very rarely.

Finally, we have a build() method, which is responsible for building the block content. It needs to return a render array (just like our Controller did), and as you can see, we are using our injected service and returning the same greeting. That is pretty much what we need to do in order to achieve our goal. There are other important aspects to block plugins we will cover later, such as caching and access, but we have specific chapters for those topics.

Block configuration

Before we close the book on our custom block plugin, let's take a look at how we can add a configuration form to it. This way, we can practice using some more Form API elements and see how we can store and use block configuration.

Even though our functionality is complete (for the moment), let's imagine that we need a Boolean-like control on our block configuration so that when an admin places the block, they can toggle something, and that value can be used in the build() method. We could achieve this with three to four methods in our plugin class.

First, we would need to implement the defaultConfiguration() method, in which we describe the items of configuration that we are storing for this block and the default values for these items. So, we could have something like this:

/**
 * {@inheritdoc}
 */
public function defaultConfiguration() {
  return [
    'enabled' => 1,
  ];
}

We return an array of keys and values that will be in the configuration. Also, since we said we are going with a Boolean field, we use the number 1 as the value to a fictitious key named enabled.

Next, we would need to implement the blockForm() method, which provides our form definition for this configuration item:

/**
 * {@inheritdoc}
 */
public function blockForm($form, FormStateInterface $form_state) {
  $config = $this->getConfiguration();
  $form['enabled'] = array(
    '#type' => 'checkbox',
    '#title' => $this->t('Enabled'),
    '#description' => $this->t('Check this box if you want to enable this feature.'),
    '#default_value' => $config['enabled'],
  );
  return $form;
}

With the appropriate extra use statement at the top of the file:

use Drupal\Core\Form\FormStateInterface;  

As you can see, this is a typical Form API definition for one form element of the type checkbox. Additionally, we are using the handy getConfiguration() method of the parent class to load up the configuration values that get saved with this block. If none have been saved, note that the enabled key will be present in it with the default value we set above (1).

Lastly, we would need the submit handler that will do what's necessary to "store" the configuration. I used inverted commas because we don't actually have to do anything related to storage, but just map the value submitted in the form to the relevant key in the configuration. The block system does it for us:

/**
 * {@inheritdoc}
 */
public function blockSubmit($form, FormStateInterface $form_state) {
  $this->configuration['enabled'] = $form_state->getValue('enabled');
}

It couldn't be simpler than this. Now, if we placed our custom block somewhere, the form we are presented with would incorporate our form element that allows us to toggle the enabled key. What remains to be done is to make use of this value inside the build() method. We could do that similarly to how we loaded the configuration values inside the buildForm() method:

$config = $this->getConfiguration();

Alas, we don't really need this configuration in our example block, so we won't be adding it to our code. However, it is important for you to know how to do it, so we covered it here. Moreover, before moving on, I also want to specify that you can use an optional method to handle validation on the configuration form. The method name is blockValidate(); it has the same signature as blockSubmit() and works the same way as the validation handler we saw when we built our standalone form. So, I won't repeat that here.

 

Working with links

One of the principal characteristics of a web application is the myriad of links between its resources. They are in fact the glue that brings the internet together. So, in this section, I want to show you a few common techniques used while working with links programmatically.

There are two main aspects when talking about link building in Drupal—the URL and the actual link tag itself. So, creating a link involves a two-step process, but can also be shortened into a single call via some helper methods.

The URL

URLs in Drupal are represented with the Drupal\Core\Url class, which has a number of static methods that allow you to create an instance of it. The most important of these is ::fromRoute(), which takes a route name, route parameters (if any are needed for that route), and an array of options to create a new instance of Url. There are other such methods available that turn all sorts of other things into a Url, most notably the ::fromUri() method, which takes an internal or external URI. These methods can be very helpful, especially when dealing with dynamically obtained data. However, when hardcoding, it›s always best to work with route names because that allows you to later change the actual path behind that route without affecting your code.

There are many options that can be passed to Url when instantiating it inside the $options array. You can pass an array of query parameters, a fragment, and others. These will then help construct a URL as complex as you need without having to deal with strings yourself. I suggest that you check out the documentation above the ::fromUri() method because it describes them all. Also, keep in mind that the options are pretty much the same, regardless of the method that you use to create the Url object.

The link

Now that we have a Url object, we can use it to generate a link. We can do this in two ways:

  • Use the LinkGenerator service (named link_generator) and call its generate() method by passing the link text and the Url object we have obtained. This will return a GeneratedLink object, which contains the actual string representation of the link as well as some cache metadata.
  • Use the \Drupal\Core\Link class, which wraps a render element (we will talk more about render elements in Chapter 4, Theming) to represent the link.

Let's take a look at an example of both, from start to finish.

Consider this example of generating a link using the service:

$url = Url::fromRoute('my_route', ['param_name' => $param_value]);
$link = \Drupal::service('link_generator')->generate('My link', $url);

We can then directly print $link because it implements the __toString() method.

Now, consider this example of generating a link using the Link class:

$url = Url::fromRoute('my_other_route');
$link = Link::fromTextAndUrl('My link', $url);

We now have $link as a Link object whose toRenderable() method returns a render array of #type => 'link'. Behind the scenes, at render time, it will also use the link generator to transform that into a link string.

If we have a Link object, we can also use the link generator ourselves to generate a link based on its own data:

$link = \Drupal::service('link_generator')->generateFromLink($linkObject); 

Which way to link?

As we saw, we have a number of ways to create links and URL representations, but when it comes to creating a link, which method should we use? There are advantages and disadvantages to each one.

When it comes to the URL, as mentioned, it's a good idea to stick to hardcoding routes rather than URIs. However, if you are working with dynamic data, such as user input or stored strings, the other methods are perfectly valid. I recommend that you look at the Url class in detail because you will be using it quite a bit as you develop Drupal modules.

Regarding the actual links, using the service to generate a link means that you are creating a string at that point in the code. This means that it cannot be altered later in the process. Instead, using the Link class falls nicely in line with the entire render array rationale of delaying the actual generation to the last possible moment. We will talk more about render arrays later on. Generating links early on could also have consequences with the render system and cache bubbling, so it's always better to stick with the Link objects or render arrays with #type => 'link'.

When it comes to entities, you can and should use the helper methods on the base entity classes to generate links and URLs to these entities. We will talk more about entities later in this book.

 

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.

 

Summary

In this chapter, we covered a great deal of info about the things you need to know when developing Drupal 9 modules. The first thing we did was create our very own module skeleton that can be installed on a Drupal site. Then, we saw how to create a new page at a specific path (route) and show some basic data on that page. Nothing too complex, but enough to illustrate one of the most common tasks you will do as a module developer. We then took that to a new level and abstracted the logic for that data calculation into a service. Not only that, but we also saw how we can use that service and, more importantly, how we should use it. Next, we saw how we can work with the Form API to allow administrators to add some configuration to the site. Also, since we talked about forms, we saw how we can alter existing ones defined by other modules—a useful technique for any module developer.

Next, we created our first custom block, which allowed us to reuse our service and be more flexible with where we show our data.

Then, we looked at how to create URLs and links programmatically. In the functionality we built in this module, we don't need any links, yet. However, it is common practice to work with them, so we had to learn early how to generate links and work with URLs properly in Drupal.

In the last section, we explored the Symfony Event Dispatcher component, something that allows us to dispatch and subscribe to events. We saw some examples of how we can subscribe to one of the main Kernel events in order to redirect the page, but we also saw how to dispatch our own event. The latter was meant to allow subscribers to make changes to our data.

Most of the topics we covered in this chapter were meant to give you an initial boost and the tools to start developing modules in Drupal 9. They represent the absolute most common things—I believe—that any new Drupal developer encounters and has to do.

In the next chapter, we will look at two important aspects most applications will need to use. One is logging—the better your site logs its errors and important actions, the easier it will be to debug and trace back issues. Another is mailing. Websites usually need to send out emails to users in one way or another, so it's important that we see how that works in Drupal 9.

About the Author

  • Daniel Sipos

    Daniel Sipos is a senior web developer specializing in Drupal. He's been working with Drupal sites since version 6, and started out, like many others, as a site builder. He's a self-taught programmer with many years' experience working professionally on complex Drupal 7 and 8 projects. In his spare time, he runs webomelette, a Drupal website where he writes technical articles, tips, and techniques related to Drupal development.

    Browse publications by this author

Latest Reviews

(2 reviews total)
The book is excellent in principle, but it needs some formatting corrections.
I'd like to comment - unfortunately the book is still not ready to be published.
Book Title
Unlock this book and the full library for FREE
Start free trial