An Absolute Beginner's Guide: Writing Your First Drupal 9 Module

The module system in Drupal 9 is virtually identical to the Drupal 8 module system (so if you’re coming from that ecosystem, this shouldn’t be a big jump!) However, if you’re coming from an older version of Drupal (like Drupal 7) then there are going to be some really significant differences. This post is all about preparing you for the new experience!

What is a module and why do you care?

Drupal core provides a super robust set of features out of the box. Thanks to the configuration management system you can tweak and build a lot of features for common, modern websites without ever writing a line of code. That’s super cool!

BUT there are a lot of things you might want to do that you just can’t out of the box. Let’s take generating a sitemap.xml file for SEO as an example. Drupal provides no mechanism for doing this. Instead, you need a module. Now, thankfully, in this particular example there are community created modules (contrib modules) that have been created for this purpose. For a sitemap, I typically recommend the simple sitemap module.

However, you can’t always find a module for “everything.” Sometimes there are specific features required for a site, proprietary data changes, etc. that only apply to “your” code that you still need. That’s where a custom module comes into play!

A module, at its very simplest, is a folder and one or more files that define the module for Drupal and provide functionality.

Screen Shot 2020-07-27 at 4.32.39 PM.png

At its most simple, a module file usually is a .info.yml file (to register the module with Drupal) and a .module file (to provide some basic functionality). However, there are many other possible files that could live in the module including (but not limited to):

  • a services.yml file (to define custom services)

  • a routing.yml file (to define custom routes)

  • a .install file (for install and update hooks)

  • a src directory with custom PHP classes to define plugins (like blocks, fields, etc.), controllers (to power your routes), and much more

  • a template directory to include Twig templates for features defined by your module

  • a config directory to include configuration for the features provided by your module

  • etc.

If you want to see a fully fledged module with “many” features, checkout the Lightning Workflow module. BUT for your first module, we’re not going to do anything nearly so complicated. Let’s start nice, and easy.

Drupal 8 vs. Drupal 9 Differences at a Glance

The only “real” differences between Drupal 8 and Drupal 9 module development have to do with the way you declare the compatibility and which methods you can use during your work.

The core version requirement change was actually introduced in Drupal 8.7 to help differentiate between modules that support Drupal 8 and Drupal 9. Previously you defined this in your module’s .info file using the core key (e.g. core: 8.x). Note that with this core version requirement file you can easily write modules that work for both Drupal 8 and Drupal 9 as long as you deal with deprecated code.

One of the only differences between Drupal 8.9.x and Drupal 9.0.x is the removal of all code marked as deprecated in the Drupal 8.x API. As such, if you’re writing code for Drupal 9 (and/or code that you intend to work on both Drupal 8 and 9) you have to ensure that you’re not using anything that is deprecated. You can easily find code in the Drupal 8 codebase that has been marked as deprecated by:

  • located crossed out functions in your IDE

Screen Shot 2020-07-27 at 6.49.53 PM.png
  • watching for the @deprecated annotation in code

Screen Shot 2020-07-27 at 6.50.51 PM.png

Note that if you’re writing your module in Drupal 9, you won’t find any deprecated code (yet) because all of the deprecated code from Drupal 8 has already been removed! Obviously, at some point Drupal 9 will start having deprecated again as we prep for the Drupal 10 release in or around 2022. But, if you’re writing code in Drupal 8, you’ll need to monitor for accidental use of deprecated code. I recommend using a tool like Upgrade Status to scan your custom code to ensure it’s compatible with Drupal 9!

Drupal 7 vs. Drupal 9 Differences at a Glance

There are more significant differences between Drupal 7 and 8/9 than I have time to cover in this blog post. Suffice to say, there are pretty significant differences. The largest of which are:

  • In Drupal 7 “everything” was a hook. Not so in Drupal 8

  • In Drupal 8 a significant amount of the API has been ported into classes / methods (vs. hooks) so an understanding of Object Oriented PHP is critical

  • In Drupal 8, we completely remove the PHP Template system and replace with Twig

  • In Drupal 8, we introduce dependency management via Composer

I spoke at Drupal GovCon a couple of years ago on the differences between Drupal 7 and Drupal 8 module development if you want a bit more material on the topic!

Getting Started with a New Module

Ok, now that we have some of the background information out of the way, let’s build a module! I strongly recommend that you setup for local development using Drupal VM / Lando / DDev / Docksal or your other favorite virtual machine. It will make life infinitely simpler.

First, let’s create a new module called madison_example_one. You’ll want to:

  • create a new folder in docroot/modules/custom called madison_example_one

  • create a file in that folder called madison_example_one.info.yml

Inside that file, you’ll want to populate the following YML:

name: Madison Blog Example One
description: Your first Drupal 9 module.
package: examples
type: module
core_version_requirement: ^9

Obviously, you’re welcome to rename this whatever you want. Just make sure that you consistently replace the key I’m using (madison_example_one) with your own key if you so desire.

Let’s dig into what’s happening here:

  • name: the human readable name of the module: appears on the module overview page

  • description: the human readable description of the module: appears on the module overview page

  • package: the group the module will be placed in: appears on the module overview page

  • type: module (vs. profile or theme)

  • core_version_requirement: restricts the module to Drupal 9 only

Go ahead and load up your Drupal site and visit the “Extend” page (module overview page) at /admin/modules. Just with this one file, you should see your module:

Screen Shot 2020-07-27 at 7.01.42 PM.png

I mean, this module does absolutely nothing. But hey, it shows up! And you wrote a module! So congrats, that’s pretty cool.

Let’s actually make this module do something. Let’s build a custom route (that you can visit via your web browser) that will say “Hello World.”

In order to accomplish this, we will need to add two additional fields to our custom module and enable it.

  • A routing file that defines the route (in this case, we’ll just use /hello)

  • A controller that says “Hello World” whenever you visit the route!

For the routing file:

  • Create a new file called: madison_example_one.routing.yml

The details for this file can be seen here.

The routing file is:

  • defining a new route (examples.hello) that lives at the path of /hello

  • telling Drupal to look for a controller at the defined namespace (see the _controller key)

  • requiring the permission of access content

For the controller:

  • create new directories and file:

    • src

      • Controller

        • HelloController.php

The details for this file can be seen here.

The controller has a simple method called “hello” that just returns translatable markup that says “hello world.” Not fancy. It also has a use statement to call in the base Controller and a namespace that allows Drupal to find this controller. Drupal 8 and 9 rely on something called PSR-4 autoloading that allow files like this controller to automagically get loaded into your application just by putting them in the right place. That’s why it’s so important that you make “src” lowercase and “Controller” uppercase!

Once you enable the module, you should be able to visit /hello and see something like:

Screen Shot 2020-07-27 at 7.45.47 PM.png

The really tricky thing about this process is making sure that all your things are named consistently (and correctly). Otherwise it’s just “not going to work.”

If you see an error like…

In EntityResolverManager.php line 136:                                                                    
  Class \Drupal\example\Controller\ExampleController does not exist

Then it’s a pretty good sign you have an incorrect spelling, folder structure, or namespace!

In Conclusion

Writing Drupal 9 modules isn’t that hard if you are comfortable with PHP. But as someone who learned PHP a decade ago using Wordpress and Drupal 6, the process of writing object oriented PHP is very different for me than “just tinkering around with PHP” used to be. I’ve gotten used to it. I’m pretty effective at it. I understand how it works. But I only got to that point by really extensively trying (and failing) and trying some more to get to a point that I can write functional code. And then breaking it, and figuring out why it broke, and then unbreaking it. Over and over again.

The final result of this tutorial is available on Github here.

This is what you should expect to see!

Screen Shot 2020-07-27 at 8.01.30 PM.png

Related Content