Writing My First Drupal 8 Module

Drupal 8 logo

While it’s amazing how easy it is for an experienced Drupal 7 site builder to get around using the Drupal 8 UI, the same is not true for writing code for D8. As has been made clear for years now, Drupal 8 is very different inside. Many of the hooks we know and love have gone away, most procedural code has been replaced with interfaces, classes, and methods, and there are scads of YAML (Yet Another Markup Language, .yml file extension) files that wire everything together. How does a developer get her/his footing in this slippery new landscape?

I set out to write a new contrib module, Admin Status (please visit the project page for more details and to download the source code). Its goal is to provide a way to add reminder status messages that are displayed to selected users under particular circumstances. An example message might be to display a warning, “Cron hasn’t run in over 3 hours”, to users with the administrator role. Admin Status currently doesn’t actually do that specific thing, it provides a framework for adding message plugins that can be enabled and disabled, and the plugins determine what and to whom to display a message.

For Drupal 7, we can easily imagine how this module would be constructed: 1) provide hook_admin_status_info to allow message plugins to register; 2) provide an administration page that lists the registered plugins and allows them to be enabled and disabled, saving the status to an Admin Status entry in the variable table; and 3) writing admin_status_init, which runs at the beginning of each page request and loops through all the enabled plugins, calling their hook_admin_status_view function to retrieve a message and status level, which are then supplied to drupal_set_message. Two files for the module, plus an example plugin file, and we’re done.

Where to start?

For Drupal 8, it can be difficult to find firm ground from which to start. It seems like a D8 module is composed of dozens of little files in a complex directory structure. Of course, we know that this will be a big advantage eventually, because everything is autoloaded and so Drupal can minimize its footprint and only run code needed to generate the page. But, it is hard to grasp what exactly is needed to build a particular module.

One great place to start is with the Examples module. It has completely functional modules that show how to work with the different systems inside Drupal… and Symfony, for that matter. Because it seemed to be at the heart of what I wanted to accomplish, I used the plugin_type_example module as the basis for my own.

The example shows how to find plugins that are registered in the system via annotations (special comments that include data that is compiled into Drupal’s configuration - see more about this below), as well as incidental information about how to register a route (a URL to a page) and build a page for display.

I began by copying the plugin_type_example module’s code to my module’s directory. I went through the subdirectories and files, renaming everything to match my new module’s name. From this exercise, I began to understand how things were structured and what the various .yml files specified.  

Directory Structure and Namespaces

Before even diving into the code, it was clear that a module’s directory is highly structured and fine-grained. The root usually has some .yml files and a .module file. The latter is often not needed because Drupal will find what it needs from the .yml files; but you can implement things like hook_help() in here. Sometimes a .install file is needed. If the module provides a hook-based API, then there will likely be a .api.php file too.

├── plugin_type_example.api.php

├── plugin_type_example.info.yml

├── plugin_type_example.links.menu.yml

├── plugin_type_example.module

├── plugin_type_example.routing.yml

├── plugin_type_example.services.yml

└── src

    ├── Controller

    │   └── PluginTypeExampleController.php

    ├── Plugin

    │   └── Sandwich

    │       ├── ExampleHamSandwich.php

    │       └── ExampleMeatballSandwich.php

    ├── SandwichInterface.php

    ├── SandwichPluginManager.php

    └── Tests

        └── PluginTypeExampleTest.php

Also in the module root directory are the src and, perhaps, config directories. The config folder typically will have .yml files that describe configuration that you might normally build in the UI, like node types, fields, and so on. It also may have subdirectories for database table schemas and config that is added to the system at module install time. Although not in the plugin example, some of the other examples do have this directory.

In the src directory, there are files for services and interfaces defined by the module, along with subdirectories for the other class types in the module. The directory structure is mirrored in the namespaces used inside each of the source files. If you want to find the ExampleHamSandwich plugin class in the plugin_type_example module, then you know it’s going to be in plug_in_type_example/src/Plugin/Sandwich/ExampleHamSandwich.php. Similarly, if you need to use the SandwichInterface, then you need to use \Drupal\plugin_type_example\ in your code and you know that modules/plugin_type_example/src/SandwichInterface.php will be referenced.

Dependency Injection

Right off the bat, I noticed a very important concept being used: dependency injection. If you haven’t encountered this idea before, it sounds like a mysterious buzzword. (At a session at DrupalCon a couple years ago, I heard it described as a $5 buzzword for a 25¢ idea.) The idea is that a module’s code shouldn’t have specific references to outside dependencies or services; these should all be passed in. Doing so decouples the module from the rest of the system and makes it possible to test it in isolation. It also allows for outside systems to change or be entirely swapped out without modifying code inside the module.

In Drupal’s case, a lot of this dependency injection happens by specifying the services needed by a class in the services.yml file (admin_status.services.yml, in my case). When the system instantiates an object of a class, for example, a plugin manager or a form, an arguments list in the services.yml file can specify what services should be passed into the constructor.

Plugins and Annotations

Plugins are a generic way of adding customized functionality, that are used throughout Drupal 8. Plugin subclasses replace a number of different procedural interfaces that existed in Drupal 7; think of all the hook_xxx_info hooks that we used to have to work with. Drupal has a plugin framework ([docroot]/core/lib/Drupal/Component/Plugin) that provides for creation, discovery, enumeration and individual plugin access.

To use this framework, a developer creates a plugin manager subclass, a plugin subclass, and an interface that provides the specific methods needed to use the plugin type they are creating. By subclassing from Drupal’s plugin classes, the new plugin type gets the standard features described above, and usually only needs the specific code to support the features of the particular plugin type. When a new instance of a given plugin type is needed, the developer needs only to subclass the plugin type class and implement the methods in the interface.

The next important thing I saw were the annotations that were included in each of the example module’s plugins. Annotations are a particular kind of comment included with a class declaration. These are recognized and parsed during module discovery, and any configuration information is captured and stored in the database.

Drupal’s plugin classes automatically support annotations. Plugins use annotations to provide at least a machine name, but can have much more information. In fact, for particularly simple plugins, it may be the case that no actual code is required at all, just the configuration information in the annotation.

To build the Admin Status plugin types, I copied the plugin_type_example files and renamed them. I went through the code and changed machine names, directory names and class names to match. I changed the annotation information to include a machine name and a label (human readable name). I created an AdminStatusInterface interface to describe the specific methods my plugins had to provide and  changed the constructor for the plugin manager to know about the Admin Status plugin interface.

Finally, I created the src/Plugin/AdminStatus subdirectory and added two files there. The first, AdminStatusPluginBase.php, contained an implementation of the AdminStatusInterface with default methods. Then I created DefaultMsg.php, my test case plugin, which subclassed AdminStatusPluginBase and overrode just those methods that needed changing.

Forms and Configuration

The next thing needed was to create an administration form page. I needed to place this in the administration menu under Configuration > System. I started out by looking at the config_entity_example code. Config entities are like other entities in Drupal, they have bundles and can have attached fields. They are stored in their own tables in the database, but their default form is stored as YAML, so they can be recreated, the way one might revert Features in Drupal 7. This was overkill for what Admin Status needed.

Fortunately, I came across this very nice blog post by Hillary Lewandowski about building forms, and administration configuration forms in particular. Drupal 8 has a config system that provides the kind of settings storage that is more on the level of the old Variable system in Drupal 7. These settings are saved out into YAML files, of course, so they can be committed to a code repository.

Design Patterns at Work

One thing I do want to mention, which is emphasized in Hillary’s blog, is the idea of Design Patterns. These are generalized ways of handling certain kinds of programming problems that have proved to work well in the past, and are (or will become) familiar to other programmers, making it easier to pick up the code.

In this case, I saw the Factory pattern being used as a way to support instantiating classes when the constructor may have wildly different parameters from its superclass and/or sibling classes. Rather than getting a new object by directly asking for new AdminStatusForm, this pattern has you call AdminStatus::create, which is a static function of the class, and has a uniform call signature for all ConfigFormBase subclasses. The create method takes the Services Container as its only parameter and then invokes new AdminStatusForm with its particular requirements. This allows the class constructor function to have whatever parameters it needs, so it isn’t constrained by its superclass, and it doesn’t constrain any subclass that might extend it.

The idea of Design Patterns was originally laid out in the eponymous classic book published in 1995 (you can find it at Amazon or any other technical bookseller). The four authors discuss 23 such patterns in three broad categories: Creational, Structural, and Behavioral. (The Factory pattern is in the Creational category.)

Catching Events

Over the last couple years you may have been unable to avoid the phrase Proudly Invented Elsewhere, or the discussion about Getting Off the Island, or indeed, the news that Drupal was integrating another open source project, Symfony, into its basic infrastructure. Symfony is a web application framework that handles all the details of receiving web requests and returning responses. It provides an event model to do this. You can see how this happens in great and gory detail by viewing Wim Leers’ Drupal 8 Render Pipeline talk.

The final piece of this puzzle is getting hooked into each page creation so we can display the enabled messages on each page by calling drupal_set_message. To do this, we need to register an event subscriber to catch Symfony’s kernel.request event, which is triggered when a request begins processing. This is similar to Drupal 7’s hook_init.  

Because, by this point, I actually had a clue about what I needed to produce at this step, I turned to Drupal Console, the brainchild of Jesus Manuel Olivas. This is a command line tool akin to Drush. Once you install it and then visit the top directory of your module, you can use it to generate all of the boilerplate for a particular kind of class (among other things). By running drupal generate:event:subscriber and answering a series of questions, it produced the necessary class and YAML settings. You can see all that it can do by running drupal list.

When I did this, Console created the file src/EventSubscriber/AdminStatusEventSubscriber.php, which contains the event subscriber class AdminStatusEventSubscriber, which extends EventSubscriberInterface. Console included code for static function getSubscribedEvents (which returns a list of all the events that the class would like to catch, and which method to call for each) and a boilerplate implementation of the kernel_event method to handle the kernel.event event. Console also added admin_status.eventsubscriber to the admin_status.services.yml file, including the plugin.manager.admin_status service dependency injection argument and these two lines:

tags:

     - { name: event_subscriber }

They tell the system to add our class to the list of event subscribers that Symfony should query (by calling getSubscribedEvents).

In our case, all that was needed was to change the boilerplate code in the kernel_event method so that it:

  • Gets the configuration information saved from the administration form
  • Loops through each potential plugin
    • Checks to see if the plugin is enabled
      • If so, instantiates the plugin
      • Gets the configuration data for the plugin
      • Retrieves the status and message from the plugin
      • Calls drupal_set_message with the status and message as parameters

Conclusion

For experienced Drupal developers, it may be difficult to harken back to those difficult and confusing days when first learning about Drupal hooks, the detailed knowledge needed to interact with each separate subsystem and the endless, deeply nested arrays, whose content could only be discerned with dpm() calls or a debugger. The struggle involved to master Drupal 6 and 7 subsystems left a lot of developers pretty frustrated:

The Drupal Climb

 

Source: https://web.archive.org/web/20160406122424/http://www.codem0nk3y.com/2012/04/what-bugs-me-about-modx-and-why/cms-learning-curve/

In Drupal 8, while there are more “moving parts” (individual class files, subdirectories to collect like type classes and a selection of YAML files) needed to build a module, the ideas are more uniform, which I believe will make building a Drupal site easier, with some experience, than in the past. Perhaps this can smooth out some of the steeper parts of the Drupal Learning Curve!