Introduction to module development

by Gisle Hannemyr

Whole books are written about Drupal module development. This chapter is just a brief introduction to a very complex subject, to get you started.

Table of contents

Introduction

One of the most flexible aspects of Drupal is that the framework allows for custom module development.

To do back-end Drupal development you need to understand:

To do front-end Drupal development you need to understand:

Helpful links:

Guidelines for module documentation

Below are pointers to the major sources for writing modules and documentation on Drupal.org:

Elsewhere:

Semantic versioning

Since Drupal 8, projects can be compatible with any range of Drupal core versions. The API compatibility component (e.g. 8.x) is no longer part of the numbering scheme. Put another way, a 8.x-1.0 release is the same as 1.0.0.

This is known as semantic versioning, and version number are made up of this triplet: {major}.{minor}.{patch}.

With semantic versioning, composer.json and core_version_requirement are used to define a release's requirement for Drupal core.

With semantic versioning, the branch name for the first version should be 1.0.x

See alsoRead more at semver.org. Information @Drupal.org: Change records and Transitioning from 8.x-* to semantic versioning. See also these example projects from drumm and ccjjmartin.

Run-time error reporting

While developing, some run-time errors may produce a blank page or ablank page with just the following error message. “The website encountered an unexpected error. Please try again later.” This is known to developers as the White Screen Of Death or WSOD. You may also experience that some task, such as installation of a custom module, has no effect.

To get more information about run-time errors, navigate to: Configuration» Development » Logging and errors to configure error messages to display. For development select “All messages”. For a production site, you want to set this to “None”.

You can also make use of the logger class provided by the Database logging core module when debugging a module:

\Drupal::logger('my_module')->notice('A notice.');
\Drupal::logger('my_module')->error('An error');

tipThis can be used just as one used the global watchdog() function in Drupal 7 and earlier. The Database logging module was previously called Watchdog.

To add a message to the list of messages to be displayed in the message area, in OO code use:

addMessage('A status', self::TYPE_STATUS, FALSE);
$this->messenger()->addMessage($this->t('A custom type.'), 'custom', TRUE);
$this->messenger()->addStatus('A status');
$this->messenger()->addWarning('A warning');
$this->messenger()->addError('An error');

In procedural code, you can use:

\Drupal::messenger()->addMessage('A message.');

In addition to the functions listed above, there is the Devel project that provides functions that can be used for debugging. Unlike debug output generated by the devel-functions output generated by the debug fuction does not have any access controls, so it will always display, even for the anonymous user role.

There is also PHP:

debug($var, 'var');

See alsoThere is more about debugging run-time errors on the Drupal.org site. See, for instance, these notes posted on the developer's wiki: Show all errors while developing [Drupal 7], Fixing white screens step by step and Blank pages or WSOD. In addition to the WSOD, internal server errors (status code 500) may pose a challenge when debugging.

This pair of CLI commands may also be handy when debugging. The first shows the latest entries from the database log. The second truncates the log.

$ drush ws
$ drush wd-del all -y

QA tools

A Drupal project is expected to meet certain standards. There are a couple of automatic tools that can help with this:

To install PAReview.sh, clone it from the Drupal.org git repository to a your local git repository. This should be a location outside of any webroot folders.

$ git clone --branch 7.x-1.x http://git.drupal.org/project/pareviewsh.git

To make it usable, also install these two patches (until they are committed):

PAReview.sh makes use of Composer to install all its dependencies. Make sure you have git, composer, npm and pip/pip3 installed. Also make sure that $HOME/.local/bin is on your $PATH:

$ PATH=$PATH:$HOME/.local/bin

Install the dependencies using composer:

$ composer install

To update an existing installation:

$ composer update

To be able to run the command from everywhere:

$ ln -s ~/src/gitrepo/pareviewsh/pareviewsh/pareview.sh ~/.local/bin/pareview

To see the usage information for pareview.sh do the following:

$ ./pareview.sh --help
Usage:    ./pareview.sh GIT-URL [BRANCH]
          ./pareview.sh DIR-PATH
Examples:
  ./pareview.sh http://git.drupal.org/project/rules.git
  ./pareview.sh http://git.drupal.org/project/rules.git 8.x-1.x
  ./pareview.sh sites/all/modules/rules

The online version of PAReview will review the default branch in the git repository you supply the URL to (and gripe about it), unless you specify a branch. Here is an example of how to specify the url with a specific branch name:

http://git.drupal.org/sandbox/gisleh/1834468.git 1834498-bugfix

The syntax of the strings passed to the translation function t() is checked by the Drupal translation server, and reported on the translation releases page for the project. To get to this page, click on the link View project translations in the right sidebar on the project's home page, and then on the Releases button. Clicking on the number in the Warnings column show the warnings.

Naming a module

Names are important in Drupal. A lot of what goes on under the hood of Drupal depends on naming conventions. One of these naming conventions is that in addition to a human readable name every module must have an unique machine name. The human readable name is a proper noun, so it should be capitalised. The machine name can be anything (as long as it unique), but the standard procedure is it to derive from the module's human readable name by removing the spaces and any special characters (or replacing them with underscores), if necessary abbreviating some parts of the human readable name, and converting the resulting string to lower case.

The machine name will be used for machine identification. It will be used to name the folder that contains the module, the files in the module, and as part of the names of the functions and variables that make up the module.

You should exercise some care when picking the module's machine name. In particular, make sure the machine name is not part of any other module's machine name, or vice versa. If there already exists a module named “foobar”, you should not use “foo”, nor “foobarbaz”, as the machine name of your module.

Drush generate

It is possible to use drush to generate a boilerplate module. The example below shows the creation of a tiny module named Autogenerated:

$ drush generate controller

 Welcome to controller generator!
 Module machine name:
 ? autogenerated
 Module name [Autogenerated]:
 Class [AutogeneratedController]:
 Would you like to inject dependencies? [No]:
 Would you like to create a route for this controller? [Yes]:
 Route name [autogenerated.example]:
 Route path [/autogenerated/example]:
 Route title [Example]:
 Route permission [access content]:

The following directories and files have been created or updated:

   …/web/modules/autogenerated/autogenerated.info.yml
   …/web/modules/autogenerated/autogenerated.routing.yml
   …/web/modules/autogenerated/src/Controller/AutogeneratedController.php

After enabling the module, visit the following path /autogenerated/example below the site home page to check that it works.

A very simple module

In this section, we're going to create a similar module to the one autogenerated by drush step by step. This is a module that is equivalent to the “Hello, World!” example that you'll find in the introductory chapter of almost any programming language text-book. But it is still a complete module that you may install and enable on any Drupal 9/10 website,

The very first thing that you need to do is to decide on a name for your module. Since this is going to implement “Hello, World!”, the human readable name of this custom module will be “Hello World”. The machine name will be “hello_world”.

We will need to create a new directory in Drupal where we'll place our module. Under the webroot, there is a directory named modules. Create a subdirectory named custom (since this will be a custom module), and create a directory inside this directory with the machine name of your module. (In this case “hello_world”). The path from the webroot to the module directory is “modules/custom/hello_world”. Then inside this directory create the following file:

  1. Create .info.yml module

You may also want to do the following to add some functionality to the module:

  1. Create .routing.yml file
  2. Create the controller file
  3. Check if it works

To create this working module, there are just four steps.

Step 1: Create .info.yml

In Drupal 9 (and later), all modules will need to have a file with the module's machine name followed by the extension .info.yml. Inside this file, we will include information about the module. How this information is used is shown in square brackets and should not be included in the actual hello_world.info.yml.

name: Hello World [The human readable name displayed on the modules list.]
description: Custom Drupal 9 module example. [Description of the module.]
core_version_requirement: ^9 || ^10 [The versions of Drupal it is compatible with.]
type: module [Stating that this is a module.]
package: Custom [Stating that this is a custom module.]
version: 1.0.0 [The version number (usually added by packaging script).]

The key “type” must be “module” for a module project. For a theme project, it must be “theme”.

The key “package” is optional. If present, it will determine the section where the module will appear in the list of modules (under the “Extend” tab). If omitted, the module will appear under “Other”.

You must omit the key “version” if the project is a contributed project hosted on Drupal.org. The correct version key will be added by the packaging script that is used to prepare the project containing the module for download.

tipThis is the only file needed for having a valid Drupal module. If this file exists, the module will show up in the list of modules, and it can be installed and uninstalled. If you don't add anything else, it will of course not do anything. But it is a valid module.

Step 2: Create .routing.yml

Next, create a routing file for the module. This file will be used by the controller to know at which URL path the module's function will execute. The name of the file will be hello_world.routing.yml. Inside this file, the following code will be placed:

hello_world:
 path: '/helloworld'
 defaults:
   _controller: 'Drupal\hello_world\Controller\HelloWorldController::hello'
   _title: 'Hello, World!'
 requirements:
   _permission: 'access content'

This tells Drupal that a route named “hello_world” (named with the module name as prefix and in this case the only route) exists and is bound to the URI “/helloworld” on the site. When that path is accessed, the “access content” permission is checked on the accessing user and, if access is granted, the “HelloWorldController::hello” method is invoked and a page is displayed with the title “Hello, World!”.

See alsoIf you want to expose content or functionality on your own URIs on a site, routing is important. However, if you're only modifying or extending existing functionality, a routing file is not required. Read more on Drupal.org: Introductory Drupal routes and controllers example.

Step 3: Create the Controller file

Next, create a new subdirectory under the module's folder which shall hold the module's source code. All external plugins, controllers, etc. are placed under this directory. Name it “src” (short for source). Create a new folder under “src” with the name “Controller”.

Drupal uses the Symfony HTTP Kernel. This is a system which gets the request and asks other systems to produce the requested output (a response object) and sends the response back to the client. The output is generated by a piece of code called the controller. In theory the controller can be either a PHP function, a method on an object or even an anonymous function, but best practice in Drupal is to use a controller class. This class is in a file named exactly after the controller provided in the routing file, “HelloWorldController.php”. This is the file containing the code that controls the functionality of the module.

Enter the following code in this file:

<?php
/**
 * An example controller.
 */
namespace Drupal\hello_world\Controller;

use Drupal\Core\Controller\ControllerBase;

class HelloWorldController extends ControllerBase {

   /**
    * Returns a render-able array for a test page.
    */
   public function hello() {
    return [
      '#markup' => $this->t('Content for the Hello World-page.'),
    ];
  }
}

tipYou should notice that the file starts with the <?php processing instruction, but it does not end with the corresponding ?>. This is not a typographic error. Drupal coding standards require files that are completely PHP to start with <?php and omit the closing ?>. This is done for several reasons, most notably to prevent the inclusion of whitespace from breaking HTTP headers.

technicalIn this tiny example, it is not necessary to declare "ControllerBase" as a parent class or to have the "use"-statement to load this parent class, since no methods from the ControllerBase is used in the class. However, they are included in code example because it shall serve as a template for a typical controller class.

With only the routing file and the page controller, we made a page available on our site at “/helloworld” that outputs “Content for the Hello World-page.” on a page titled “Hello, World!”.

See alsoAlso read the official documentation DO: Building a page controller.

Step 4: Check if it works

Login to the Drupal site and enable the module. To check if it functions properly, visit the path you specified in the routing file. If you get the error message “Page Not Found”, then refresh the cache:

$ drush cr

Check again. It should function properly this time.

See alsoSee the chapter Creating a bulk action plugin for another complete example of creating a simple module.

Creating a simple service-provider

A service-provider is a PHP class with some code that provides a single specific functionality for the website. You can easily access each service-provider and use its functionality wherever you need it. This is called service-oriented architecture which is not unique to Drupal, Symfony or even PHP.

In this section, it will be shown how to create a module project to provide a service-provider that provides a “Hello, World!” service to other modules.

The machine name (see above) of this module shall be hello_service. Create a the top directory for this in “modules/custom”.

Step 1: Create .info.yml

Create the .info.yml file (hello_service.info.yml).

name: 'Hello Service'
type: module
description: 'An example module that declares a service.'
core: 8.x
package: Examples
version: 0.1

This file is very similar to the .info.yml created for the “Hello World” module above. However, the “Hello Service” project shall not need a routing file and a controller, as it will not do anything beyond providing a service.

Step 2: Create .services.yml

The services provided by a module is defined in a file named mymodule.services.yml, where mymodule must be a unique module name.

Create the .services.yml file (hello_service.services.yml).

services:
    hello_service.say_hello:
        class: Drupal\hello_service\TheService

The string hello_service.say_hello is the service name. The name must be the module name (hello_service) concatenated with a (here: say_hello).

The class with namespace becomes class: Drupal\hello_service\TheService. The code for this lives in a file named TheService.php in the src folder. The class name can be chosen freely.

Any dependency can be also be added to this file with “@” indicating another service. For example:

        arguments: ['@current_user', ]  

For the first version, of the Hello Service, there are no dependencies.

See alsoFor a detailed explanation on the structure of the services file please visit Drupal.org: Structure of a service file. You may also like to read about the Symfony Service Container.

Step 3: Create the service class

Create the file TheService.php in the src folder.

<?php

namespace Drupal\hello_service;

/**
 * TheService is a simple example of a Drupal service.
 */
class TheService {

  /**
   * Returns Hello, World!.
   */
  public function sayHelloWorld() {
    return 'Hello, World!';
  }

}

This is simple class that provides the service by means of a public function.

Step 4: Check if it works

You should be able to access the service from anywhere.

For a quick test of the service, enable the Devel PHP module, navigate to Devel » Devel settings » Toolbar settings and enable “Execute PHP”. Then Devel » Execute PHP and run the following code:

$our_service = \Drupal::service('hello_service.say_hello');
print($our_service->sayHelloWorld());

You should see the text “Hello, World!” in the help area of your administrative theme.

The example below shows how a client module may use the service in its controller.

<?php
/**
 * Controller using the hello_service.
 */
namespace Drupal\hello_client\Controller;
use Drupal\Core\Controller\ControllerBase;

class HelloClientController extends ControllerBase {

   /**
    * Returns a render-able array for a test page.
    */
  public function hello() {
    $our_service = \Drupal::service('hello_service.say_hello');
    return [
      //'#title' => $this->t('Hello, World!'),
      '#markup' => $this->t($our_service->sayHelloWorld()),
    ];
  }
}

You may, for instance replace the Controller class of “Hello World” with this. If you do this, make sure you also change the routing.

Interacting with the database

[TBA]

Variables [D7]

[This section requires updating to be useful for Drupal 9 and later. See zyxware.com: How to Configure Variables in Drupal 8.]

HTTP is stateless, which means that there is no default mechanism to transfer information from one web page to another. To allow pages to share information, there are two mechanisms available: Drupal variables, and session variables.

Drupal variables

Drupal variables (aka. database variables, persistent variables, or just variables), are variables that are persistent and global. They are shared by all pages and sessions.

These variables are set when Drupal is boostrapping a page. They may be read from the database ({variable} table), or from the site's settings.php file. The values in settings.php take precedence.

The following three functions below are used to interact with Drupal variables:

variable_set($name, $value)
variable_get($name, $default = NULL)
variable_del($name)

Function variable_set set the value of the variable to the value passed in the second argument. A variables name must start with the machine readable name of the module that owns it, followed by an underscore. If a module is named mymodule, a variable that holds and integer setting some sort of timeout value could be named mymodule_timeout. The follwing function call sets the value of this variable to 3600.

variable_set('mymodule_timeout', 3600);

Function variable_get returns the current value of a variable. The second parameter let you pass a default value that will be returned if the variable has never been set.

$ret0 = variable_get('mymodule_timeout', 900);
variable_set('mymodule_timeout', 3600);
$ret1 = variable_get('mymodule_timeout', 1800);
variable_del('mymodule_timeout');
$ret2 = variable_get('mymodule_timeout', 1800);
echo "$ret0 $ret1 $ret2";

The output of the above code fragment would be: “900 3600 1800”.

If your module creates variables, it needs to remove those from the database when the module is uninstalled. Below is an example showing boilerplate code inside an implementation of hook_uninstall by a module named “mymodule” to remove all the variables created by that module. Note that the method used for deletion involves the use of the module machine name in combination with a wildcard and is considered to be quick and dirty.

/**
 * Implements hook_uninstall().
 */
function mymodule_uninstall() {
  // Delete my variables.
  db_delete('variable')
  ->condition('name', 'mymodule_%', 'LIKE')
  ->execute();
  cache_clear_all('variables', 'cache');
}

Some Drupal programmers argue that the boilerplate code presented above should be deprecated as unsafe because it may interfere with the Drupal variable namespace of another module. However, if one exercises some care when picking the machine name for the module, this should not happen.

noteIf you use the machine name mymodule and there exists another module named mymodule_somethingelse, and both are installed at the same Drupal website, the wildcard construct used in the boilerplate will wipe out the Drupal variables owned by both modules when you uninstall the module with the shortest name. To prevent this from happening the wildcard construct should not be used. Instead make sure your module keeps track of all the variables it creates, and deletes each one explicitly with repeated calls to del_variable.

Session variables

[This section requires updating to be useful for Drupal 9 and later. See Drupaldump.com: How to get and set session data in drupal 8.

Session variables are not global, and only persistent for the duration of a session, where a session lasts from the point a user logs on a site, and until that user logs out again or closes his or her browser.

This means that a session variable is suitable for tracking state for a single user during a single visit, while that user moves from page to page on the site.

tipIn addition to tracking the session of logged in users, the session variables tracked by Drupal will also track the default anonymous user (user 0). While all anonymous visitors share this user id, they all will have different session variables unless they also share the same browser instance.

You may store any variable you want to keep track of in this manner in a named location in the session array, and retrieve it later in the session. Example:

Session variables only work with arrays. Setting a session variable to a scalar value has no effect.

$_SESSION['foo'] = 'This will not work;
$_SESSION['bar'] = array('OK');
$_SESSION['mymodule']['a'] = 'OK';
…
echo $_SESSION['foo'];
echo $_SESSION['bar'][0];
echo $_SESSION['mymodule']['a'];

The first echo-statement will not output anything, while the last two echo-statements will output “OK”.

See also this.

Administrator's interface

[TBA]

Final word

Here are links to some places to go to learn more about Drupal module development:

Source.

See alsoWhile you're developing the site, you will probably have a lot of monitoring turned on, cache turned off, etc. When you go from development to production, you may want to tune your site for speed, rather than debugging. Dominique De Cooman has written a go live checklist that may come handy when turing a development site into a production site.


Last update: 2022-04-23 [gh].