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
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
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.