Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Drupal 9 Module Development
Drupal 9 Module Development

Drupal 9 Module Development: Get up and running with building powerful Drupal modules and applications , Third Edition

eBook
€28.99 €32.99
Paperback
€41.99
Subscription
Free Trial
Renews at €18.99p/m

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning
OR
Modal Close icon
Payment Processing...
tick Completed

Billing Address

Table of content icon View table of contents Preview book icon Preview Book

Drupal 9 Module Development

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.

Left arrow icon Right arrow icon
Download code icon Download Code

Key benefits

  • Explore the essential Drupal 9 APIs for module development
  • Learn how to implement data modeling, caching, architecture, and much more in your Drupal applications
  • Discover what's new in the latest Drupal core release

Description

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!

Who is this book for?

If you are a Drupal developer looking to learn Drupal 9 to write modules for your sites, this book is for you. Drupal site builders and PHP developers with basic object-oriented programming skills will also find this book helpful. Although not necessary, some Symfony experience will help with understanding concepts easily.

What you will learn

  • Develop custom Drupal 9 modules for your applications
  • Master different Drupal 9 subsystems and APIs
  • Model, store, manipulate, and process data for effective data management
  • Display data and content in a clean and secure way using the theme system
  • Test your business logic to prevent regression
  • Stay ahead of the curve and write PHP code by implementing best practices

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date : Aug 14, 2020
Length: 626 pages
Edition : 3rd
Language : English
ISBN-13 : 9781800200548
Concepts :
Tools :

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning
OR
Modal Close icon
Payment Processing...
tick Completed

Billing Address

Product Details

Publication date : Aug 14, 2020
Length: 626 pages
Edition : 3rd
Language : English
ISBN-13 : 9781800200548
Concepts :
Tools :

Packt Subscriptions

See our plans and pricing
Modal Close icon
€18.99 billed monthly
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Simple pricing, no contract
€189.99 billed annually
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just €5 each
Feature tick icon Exclusive print discounts
€264.99 billed in 18 months
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just €5 each
Feature tick icon Exclusive print discounts

Frequently bought together


Stars icon
Total 115.97
Mastering Drupal 8
€36.99
Drupal 8 Theming with Twig
€36.99
Drupal 9 Module Development
€41.99
Total 115.97 Stars icon

Table of Contents

19 Chapters
Chapter 1: Developing for Drupal 9 Chevron down icon Chevron up icon
Chapter 2: Creating Your First Module Chevron down icon Chevron up icon
Chapter 3: Logging and Mailing Chevron down icon Chevron up icon
Chapter 4: Theming Chevron down icon Chevron up icon
Chapter 5: Menus and Menu Links Chevron down icon Chevron up icon
Chapter 6: Data Modeling and Storage Chevron down icon Chevron up icon
Chapter 7: Your Own Custom Entity and Plugin Types Chevron down icon Chevron up icon
Chapter 8: The Database API Chevron down icon Chevron up icon
Chapter 9: Custom Fields Chevron down icon Chevron up icon
Chapter 10: Access Control Chevron down icon Chevron up icon
Chapter 11: Caching Chevron down icon Chevron up icon
Chapter 12: JavaScript and the Ajax API Chevron down icon Chevron up icon
Chapter 13: Internationalization and Languages Chevron down icon Chevron up icon
Chapter 14: Batches, Queues, and Cron Chevron down icon Chevron up icon
Chapter 15: Views Chevron down icon Chevron up icon
Chapter 16: Working with Files and Images Chevron down icon Chevron up icon
Chapter 17: Automated Testing Chevron down icon Chevron up icon
Chapter 18: Drupal Security Chevron down icon Chevron up icon
Other Books You May Enjoy Chevron down icon Chevron up icon

Customer reviews

Top Reviews
Rating distribution
Full star icon Full star icon Full star icon Full star icon Half star icon 4.6
(10 Ratings)
5 star 90%
4 star 0%
3 star 0%
2 star 0%
1 star 10%
Filter icon Filter
Top Reviews

Filter reviews by




Amazon Customer Jun 25, 2021
Full star icon Full star icon Full star icon Full star icon Full star icon 5
This is well written and an easy read. A really good introduction and reference work. I was expecting a heavy read, but found it easy going. I'm a novice wanting to explore the subject and found this work ideal.
Amazon Verified review Amazon
Amazon Customer Dec 31, 2020
Full star icon Full star icon Full star icon Full star icon Full star icon 5
Very nice set of samples, getting in deepth with Drupal 9
Amazon Verified review Amazon
Hilmar Kári Hallbjörnsson Dec 14, 2020
Full star icon Full star icon Full star icon Full star icon Full star icon 5
When I was preparing for teaching Open-source development class at Reykjavik University, this book was my first choice of teaching. I agree that there are some minor things here and there that I would tweak, the code examples on Github are always "end-of-chapter" solutions, therefor it's sometimes hard for students to find help/assistance from there (more code than they are expecting), but all in all, a very well written book where I could reference chapter/page and the students would understand very clearly.Both for novices and experts alike, this book suits most.
Amazon Verified review Amazon
Jouni Mäkeläinen Jan 04, 2022
Full star icon Full star icon Full star icon Full star icon Full star icon 5
Even though book is namely about module development, it's a quite complete guide to different aspects of Drupal architecture in a easily readable fashion. As such it advances deeper understanding of the CMS platform and is a great addition to the scattered online documentation.
Amazon Verified review Amazon
Oliver Jul 12, 2021
Full star icon Full star icon Full star icon Full star icon Full star icon 5
I have been working with Drupal for about 1.5-2.0 years. Clearly you don't want this book as a beginner. However, it can be good even if you don't anticipate actually building modules. The reason is that it provides some solid coverage of how Drupal works; at that is not easily found in many other resources that you will find on the web. I would aim to be at a level where you are comfortable with Drupal 8.8 forward (e.g. 9 or 10 are fine too) in the sense of building a basic website, adding contributed modules and activating modules, and having an awareness of the CSS, JS, HTML, TWIG, presentation layers roles versus the core application code and database. At that point aim to read this book in a 'skim' rather than study fashion so you see how Drupal is working under the hood from an overview of this book. Some of the stuff will be too deep to 'get' on a first read but you will be a whole lot smarter and more comfortable with basic Drupal from having done so. And if you do aim to build modules or even just tweak existing modules close to what you want to make them 'nail it' for your needs, this book should get you there. At that point, think of it as much as a reference volume as a cover to cover read because you will probably have a pretty good idea from the 'skim through' the basic area you need to work with and then just look up the exact syntax guidance relevant to that need.If you are just starting out and aren't a particularly technically experienced person in programming generally, this is probably NOT the book for you. But if you have the basics reasonably under your belt, this will get you to the level where you are probably going to be employable.
Amazon Verified review Amazon
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

How do I buy and download an eBook? Chevron down icon Chevron up icon

Where there is an eBook version of a title available, you can buy it from the book details for that title. Add either the standalone eBook or the eBook and print book bundle to your shopping cart. Your eBook will show in your cart as a product on its own. After completing checkout and payment in the normal way, you will receive your receipt on the screen containing a link to a personalised PDF download file. This link will remain active for 30 days. You can download backup copies of the file by logging in to your account at any time.

If you already have Adobe reader installed, then clicking on the link will download and open the PDF file directly. If you don't, then save the PDF file on your machine and download the Reader to view it.

Please Note: Packt eBooks are non-returnable and non-refundable.

Packt eBook and Licensing When you buy an eBook from Packt Publishing, completing your purchase means you accept the terms of our licence agreement. Please read the full text of the agreement. In it we have tried to balance the need for the ebook to be usable for you the reader with our needs to protect the rights of us as Publishers and of our authors. In summary, the agreement says:

  • You may make copies of your eBook for your own use onto any machine
  • You may not pass copies of the eBook on to anyone else
How can I make a purchase on your website? Chevron down icon Chevron up icon

If you want to purchase a video course, eBook or Bundle (Print+eBook) please follow below steps:

  1. Register on our website using your email address and the password.
  2. Search for the title by name or ISBN using the search option.
  3. Select the title you want to purchase.
  4. Choose the format you wish to purchase the title in; if you order the Print Book, you get a free eBook copy of the same title. 
  5. Proceed with the checkout process (payment to be made using Credit Card, Debit Cart, or PayPal)
Where can I access support around an eBook? Chevron down icon Chevron up icon
  • If you experience a problem with using or installing Adobe Reader, the contact Adobe directly.
  • To view the errata for the book, see www.packtpub.com/support and view the pages for the title you have.
  • To view your account details or to download a new copy of the book go to www.packtpub.com/account
  • To contact us directly if a problem is not resolved, use www.packtpub.com/contact-us
What eBook formats do Packt support? Chevron down icon Chevron up icon

Our eBooks are currently available in a variety of formats such as PDF and ePubs. In the future, this may well change with trends and development in technology, but please note that our PDFs are not Adobe eBook Reader format, which has greater restrictions on security.

You will need to use Adobe Reader v9 or later in order to read Packt's PDF eBooks.

What are the benefits of eBooks? Chevron down icon Chevron up icon
  • You can get the information you need immediately
  • You can easily take them with you on a laptop
  • You can download them an unlimited number of times
  • You can print them out
  • They are copy-paste enabled
  • They are searchable
  • There is no password protection
  • They are lower price than print
  • They save resources and space
What is an eBook? Chevron down icon Chevron up icon

Packt eBooks are a complete electronic version of the print edition, available in PDF and ePub formats. Every piece of content down to the page numbering is the same. Because we save the costs of printing and shipping the book to you, we are able to offer eBooks at a lower cost than print editions.

When you have purchased an eBook, simply login to your account and click on the link in Your Download Area. We recommend you saving the file to your hard drive before opening it.

For optimal viewing of our eBooks, we recommend you download and install the free Adobe Reader version 9.