In this tutorial we will create a module for Drupal, the focus here will be on understanding the structure of the module itself, not something super complex.

What is Drupal?

Drupal is an open source CMS (Content management system) through which you can build modern websites. Be they Personal blogs or an e-commerce.

What are modules?

A module is code that changes or adds functionality to Drupal. You can use community-created devices or create your own.

Drupal Vs. WordPress

It is impossible to name a CMS without also mentioning WordPress, the process of choosing the right CMS is an extremely important step. Choose the wrong platform and you’ll have an uphill battle right at the start of your project. Overall, Drupal is a pretty solid CMS, has a lot of features, and is optimized for top-notch performance and security from day one. it is very flexible, but has a steeper learning curve. If you don’t have time to learn to code and want to build a website as quickly as possible, consider using WordPress.

Starting the project

For this Project we will use we will need Lando and Composer.

Let’s Start a new Drupal project with composer:

composer create-project drupal/recommended-project drupal_project

We open the project directory:

cd drupal_project

And we create the Lando configuration:

lando init

In the following screens we select the options:

# From where should we get your app's codebase?
❯ current working directory
# From where should we get your app's codebase? current working directory
# What recipe do you want to use?
❯ drupal9
# Where is your webroot relative to the init destination?
web
# What do you want to call this app?
drupal_project

And we can start the project with:

lando start

Take your time, this process may take a while depending on the docker images that will be downloaded and your internet connection.

Once finished, you will see the project information on the screen, but you can view it at any time with:

lando info

To speed up the installation process we will use drush, which is a command line tool for Drupal.

First, we install drush:

lando composer require drush/drush

And install Drupal:

lando drush site:install --db-url=mysql://drupal9:drupal9@database/drupal9 -y

The admin password will appear on the screen, don’t forget to write it down.

Like magic we have Drupal installed, you can access it at https://drupal_project.lndo.site.

Drupal initial screen shoot

And with that we have a development environment for Drupal configured.

Creating the module

First of all

Before starting here is an important tip, Drupal creates page caches and sometimes our changes seem to have no effect, it is very important to clear this cache to be sure, just use the command:

lando drush cr

The module idea

Remembering that the focus here is for you to get a sense of how the structure of a module works, we will not create anything complex for now, but it will be a great start.

With this we are going to create a module to add a usage policy in the user registration form.

The module structure

This will be the structure of our module:

Module structure

Let’s go through each of these files.

Starting the module

module.info.yml

For Drupal to understand a directory as a module we need to create .info.yml.

Let’s create the module folder in web/modules/custom/policy and, important to know, the name of the directory and .yml files must follow the same pattern, in our case the policy folder will contain the policy.info.yml file.

With this information, let’s create the web/modules/custom/policy/policy.info.yml file with the following content:

name: Policy
description: "Custom module to manage the Policy terms"
package: Custom
core_version_requirement: ^8 || ^9
type: module

Only with that if we go to https://drupal_project.lndo.site/admin/modules we will already see the Policy module.

Saving our policy settings in the database

module.schema.yml

Let’s create a place where we will use to store our terms, where it will be easier to change in the future, for that we will create a configuration schema, where it will be possible to change whenever necessary.

Let’s create the web/modules/custom/policy/config/schema/policy.schema.yml file with the following content:

policy.settings:
  type: config_object
  label: "Policy Content Type Settings"
  mapping:
    title:
      type: text
      label: "Title"
    message:
      type: text
      label: "Policy terms"

module.settings.yml

The .settings.yml file will be the default value of these settings, created the moment the module is installed, so we create the web/modules/custom/policy/config/install/policy.settings.yml file with the following contents:

title: "Policy terms"
message:
  "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam a facilisis dolor, aliquam ullamcorper odio. Ut sollicitudin imperdiet ante, quis placerat sapien. Fusce sed vestibulum nisi. Morbi vel rhoncus dui, eu semper dolor. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Sed pulvinar lorem eros, et fringilla arcu blandit nec. Praesent sem nunc, hendrerit vitae arcu at, rhoncus sagittis est. Fusce ultrices venenatis rhoncus.
  Duis euismod, elit semper ultrices molestie, eros elit cursus diam, eu cursus justo urna vel lacus. Donec ornare efficitur pulvinar. Suspendisse pulvinar ipsum ipsum. Vestibulum eu fermentum felis, ut accumsan nibh. Proin blandit urna arcu, at posuere nunc hendrerit at. Sed finibus diam vel vulputate mollis. Quisque feugiat iaculis aliquet. Duis auctor enim a risus tincidunt bibendum. Proin augue tellus, finibus quis lorem interdum, mattis condimentum nunc. Nam et condimentum nibh. Sed eleifend sapien a massa sollicitudin vestibulum. Donec fringilla elit nisi, et pretium dolor dapibus in.
  Aliquam ac est vestibulum, tempus odio quis, tempor leo. Quisque dui eros, laoreet id diam ac, consectetur ultrices lorem. Duis augue ante, venenatis ac mollis sed, posuere in purus. Vivamus magna tortor, fringilla eget felis sed, mattis finibus magna. Proin mauris dui, tempor ut ante id, aliquam ultrices nisi. Aenean pulvinar sapien at enim sagittis vehicula. Donec tristique sapien eget felis tincidunt, ut commodo magna pulvinar. Quisque vel tempor dui.
  Phasellus luctus dolor sed odio pharetra, et interdum libero sollicitudin. Proin facilisis justo ac felis bibendum, ut commodo tellus lacinia. Fusce viverra eget augue nec aliquam. Quisque leo tortor, volutpat a facilisis tincidunt, molestie ut ante. Maecenas ornare commodo lacus, nec lobortis eros finibus eget. Nulla at nulla nec orci vehicula egestas semper viverra tellus. In scelerisque neque ac urna vulputate aliquet. Maecenas ultrices imperdiet ligula, eu commodo nisi. Praesent sodales felis metus, sed ultrices ipsum sodales ut. Pellentesque faucibus ligula risus, at ornare metus varius sit amet. Donec mollis non urna ut auctor. In imperdiet arcu ex, non vulputate tortor bibendum rutrum. Phasellus convallis metus in imperdiet finibus. Aenean maximus eget nibh quis interdum.
  Fusce dictum, metus at tempus tincidunt, tortor tortor porta leo, non aliquam metus arcu in sem. Pellentesque vel nulla orci. Aliquam dolor elit, accumsan vel libero et, viverra auctor augue. Sed vulputate volutpat leo, nec euismod mauris congue ullamcorper. Curabitur quis diam vestibulum, aliquam nisi nec, vehicula lacus. Sed id luctus lorem. Praesent et ante vel arcu hendrerit efficitur vitae eu augue. Phasellus vulputate metus ac sapien cursus mattis. Donec nisl enim, ornare id tortor vitae, fermentum sodales sem. Nam et ligula nec purus bibendum scelerisque. Aliquam quis enim ut nibh placerat facilisis vestibulum sed arcu. Nunc eu orci eu nunc ultricies blandit eget a lorem."

Creating our policy page

To display this content on a page we will need to create a controller and a route for it to be displayed in /policy of our site.

Creating the controller

We create the web/modules/custom/policy/src/Controler/PolicyController.php file with:

<?php

namespace Drupal\policy\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Config\ConfigFactoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Controller for display a Policy terms.
 */
class PolicyController extends ControllerBase {

  /**
   * Config object for PolicyController configuration.
   *
   * @var \Drupal\Core\Config\Config
   */
  protected $config;

  /**
   * PolicyController constructor.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   Configuration object factory.
   */
  public function __construct(ConfigFactoryInterface $config_factory) {
    $this->config = $config_factory->get('policy.settings');
  }

  /**
   * {@inheritdoc}
   *
   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
   *   The Drupal service container.
   *
   * @return static
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('config.factory')
    );
  }

  /**
   * {@inheritDoc}
   */
  public function content() {
    $message = $this->config->get('message');
    return [
      '#type' => 'markup',
      '#markup' => $message,
    ];
  }
}

Creating the route

Let’s also turn into a route file where we will call our controller displaying the policies in web/modules/custom/policy/policy.routing.yml with:

policy.content:
  path: "/policy"
  defaults:
    _controller: '\Drupal\policy\Controller\PolicyController::content'
    _title: "Policy terms"
  requirements:
    _permission: "access content"

And accessing /policy we can already see our policies page.

Meet the Hooks

Hooks are methods that manage to change a default behavior of Drupal, as well as the names of the files they must follow a name pattern to work correctly.

In our case we will create a hook to change the functioning of the new user registration page, located in /user/register to add a checkbox containing a link to the site’s policies.

module.module

The .module file is where we will create our hooks, it is treated as a .php file but we must keep the name pattern, for that we will create the web/modules/custom/policy/policy.module file with:

<?php

use Drupal\Core\Url;
use Drupal\Core\Form\FormStateInterface;

/**
 * Implements hook_form_user_register_form_alter().
 */
function policy_form_user_register_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $form['policy_terms'] = [
    '#type' => 'checkbox',
    '#default_value' => FALSE,
    '#title' => t('I have read and accept <a href="@policy_terms"  target="_blank" }" >the policy terms</a>.', ['@policy_terms' => Url::fromRoute('policy.content')->toString()]),
    '#required' => TRUE,
  ];
}

Notice that we are not exactly using pure HTML, we are using the Drupal API to add an element and it takes care of generating the code on the page, if we go to our registration page we will see the checkbox already appearing.

Opening the policy in a modal

As much as our checkbox is already working, I don’t see the need to open a new page by clicking on the the policy terms link, but I still think it’s important to have the /policy page to use elsewhere, and this is where Drupal shines again, let’s modify our policy.module file so that the page is opened in a modal, and just make the following change:

-   '#title' => t('I have read and accept <a href="@policy_terms"  target="_blank" }" >the policy terms</a>.', ['@policy_terms' => Url::fromRoute('policy.content')->toString()]),
+   '#title' => t('I have read and accept <a href="@policy_terms"  target="_blank" class="use-ajax" data-dialog-options="{&quot;width&quot;:&quot;60vw&quot; }"  data-toggle="modal" data-dialog-type="modal">the policy terms</a>.', ['@policy_terms' => Url::fromRoute('policy.content')->toString()]),

Remember to clear cache before testing.

And so, like magic, Drupal opens the page in a modal, and still keeps our route.

Updating our settings

With everything created and working, we still have to create a way to change our policies when necessary, for that we create a file web/modules/custom/policy/src/Form/PolicySettingsForm.php with:

<?php

namespace Drupal\policy\Form;

use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * Defines a form to update the Policy message config.
 */
class PolicySettingsForm extends ConfigFormBase {

  /**
   * {@inheritDoc}
   */
  public function getFormId() {
    return 'policy_form_update';
  }

  /**
   * {@inheritDoc}
   */
  protected function getEditableConfigNames() {
    return [
      'policy.settings',
    ];
  }

  /**
   * {@inheritDoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    // Form Constructor.
    $form = parent::buildForm($form, $form_state);
    $config = $this->config('policy.settings');
    $form['message'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Update Policy Terms'),
      '#default_value' => $config->get('message'),
      '#description' => $this->t('Write policy terms'),
    ];
    return $form;
  }

  /**
   * {@inheritDoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $config = $this->config('policy.settings');
    $config->set('message', $form_state->getValue('message'));
    $config->save();
    return parent::submitForm($form, $form_state);
  }
}

And we go back to the web/modules/custom/policy/policy.routing.yml file and at the end of it we add:

...

policy.form_update:
  path: "/admin/config/content/policy"
  defaults:
    _form: '\Drupal\policy\Form\PolicySettingsForm'
    _title: "Update Policy Terms"
  requirements:
    _role: "administrator"

So when we access the address /admin/config/content/policy with an admin user we will see the following page:

Editing site policies

To make it even easier, let’s create a link inside the administrative panel so we can access these settings, so we don’t always need to remember this address when we change it.

And like everything so far, just create the web/modules/custom/policy/policy.link.menu.yml file with:

policy.form_update:
  title: "Policy Terms Settings"
  description: "Update policy terms"
  route_name: policy.form_update
  parent: system.admin_config_content
  weight: 10

So if we click on Configuration in the admin menu we will see the Policy Terms Settings link.

With that we have our module finished.

The code for this tutorial is in this GitHub repository.