Introduction to module development
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
- Guidelines for module documentation
- Semantic versioning
- Run-time error reporting
- QA tools
- Naming a module
- Drush generate
- A very simple module
- Creating a simple service-provider
- Interacting with the database
- Variables [D7]
- Administrator's interface
- Final word
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:
- PHP
- YML
To do front-end Drupal development you need to understand:
- HTML
- CSS
- Twig
- JavaScript
Helpful links:
Guidelines for module documentation
Below are pointers to the major sources for writing modules and documentation on Drupal.org:
- Style, structure, and guidelines
- Online documentation structure
- Module documentation guidelines.
- README Template.
- Content style guide
- Coding standards
- Drupal.org style and content guidelines
- Writing info.yml files
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
Read 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:
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');
This
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');
There 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:
- PAReview.sh (install locally)
- PAReview (online version)
- Coder
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):
- Use pip or pip3 (only for Ubuntu 20.04 LTS).
- Undefined method.
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
in the right
sidebar on the project's home page, and then on the
button. Clicking on
the number in the 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:
- Create
.info.yml
module
You may also want to do the following to add some functionality to the module:
- Create
.routing.yml
file - Create the controller file
- 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.
This 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!”.
If 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.'), ]; } }
You
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.
In 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!”.
Also 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 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.
For 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
and enable “Execute PHP”. Then 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.
If 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.
In 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”.
Administrator's interface
[TBA]
Final word
Here are links to some places to go to learn more about Drupal module development:
- Drupal.org: Examples for Developers
- API.Drupal.org: API reference
- SymfonyCasts.com: Drupal 8: Under the Hood
- Drupal Up: Drupal 8 Video Tutorials
- Drupal Camp Asheville 2018 (video, 1:06:47)
While 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].