Add configuration to a custom module in Drupal 8

By Ronald van Belzen | April 4, 2016

In a previous blog post I showed how to build a simple module. The only thing the module was supposed to do is produce an empty front page. The reason behind this was that no content will be published on the front page of the blog that I build. But when there is no content Drupal 8 displays a text on the frontpage that explains that there is no front page content. I wanted to suppress that behaviour.

There is a module that does exaclty what I needed, but that module is not available for Drupal 8, yet. So, I built my own solution. But what if I wanted to share my solution with others; what would I need to change? The documentation on the Drupal 8 site helps with that. We skip the part that a module name needs to be unique for now, and concentrate on the best practice of making your module configurable.

In the simple My Empty Front Page module there is no question about what can and must be configurable. The path that invokes the empty front page is the only thing we will need to change in a different environment in which the hard-coded path "/empty" has already been taken to produce some other content for that site.

A configuration field

First I create a new subdirectory "module\custom\my_empty_front_page\config\install" in my module directory and in that subdirectory I place the file "my_empty_front_page.settings.yml" with the following content:

empty_front_page_path: '/my_empty_front_page/empty'

This tells Drupal 8 that there is a setting parameter with the name "empty_front_page_path" for the module with the default value of "/my_empty_front_page/empty" (I decided that "/empty" would not be that unique, so I change the default value to a more unique path name). Now we also need to tell Drupal what the format of this variable is, because Drupal will save this variable for us and needs to know the format.

For this reason I create the new subdirectory "module\custom\my_empty_front_page\config\schema" in my module directory and in that subdirectory I place the file "my_empty_front_page.schema.yml" with the following content:

#Schema for My Empty Front Page configuration.
my_empty_front_page.settings:
  type: config_object
  label: 'My Empty Front Page setting'
  mapping:
    empty_front_page_path:
      type: string
      label: 'Setting for empty front page path'

This tells Drupal that we have a "my_empty_front_page.settings" configuration containing the field "empty_front_page_path" that has the format "string".

A form

To be able to change the value for this field I first need to create a Form. For this I create a new subdirectory "module\custom\my_empty_front_page\src\Form" and in this subdirectory a new file "MyEmptyFrontPageSettingsForm.php" that contains the Form class:

<?php 
/**
 * @file
 * Contains \Drupal\my_empty_front_page\Form\MyEmptyFrontPageSettingsForm.
 */

namespace Drupal\my_empty_front_page\Form;
 
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Config\ConfigFactoryInterface;

/**
 * Displays the my_empty_front_page settings form.
 */
class MyEmptyFrontPageSettingsForm extends ConfigFormBase {
  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'my_empty_front_page_form';
  }
  
  /**
   * {@inheritdoc}
   */
  protected function getEditableConfigNames() {
    return ['my_empty_front_page.settings'];
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $config = $this->config('my_empty_front_page.settings');
    
    $form['actions']['#type'] = 'actions';
    $form['actions']['empty_path'] = array(
      '#type' => 'textfield',
      '#title' => t('Empty front page path'),
      '#default_value' => $config->get('empty_front_page_path'),
      '#required' => TRUE,
      '#description' => t('Path that the Site Information default front page setting may use for an empty front page.'),
    );
    $form['actions']['submit'] = array(
      '#type' => 'submit',
      '#value' => $this->t('Save configuration'),
      '#button_type' => 'primary',
    );

    // By default, render the form using theme_system_config_form().
    $form['#theme'] = 'system_config_form';
    
    return parent::buildForm($form, $form_state);
  }
  
  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    parent::validateForm($form, $form_state);
    
    $empty_path = $form['actions']['empty_path']['#value'];
    
    // path name must start with '/'
    if(substr($empty_path,0,1) != '/') {
      $form_state->setErrorByName('empty_path', t('The path name must start with \'/\'.'));
    }
    // path must not be '/' or '/node'
    if($empty_path == '/' || $empty_path == '/node') {
      $form_state->setErrorByName('empty_path', t('%token is not a valid path.', ['%token' => $empty_path]));
    }
    // path may not already be in use
    if(\Drupal::service('path.validator')->isValid($empty_path)) {
      $form_state->setErrorByName('empty_path', t('%token is not an available path.', ['%token' => $empty_path]));
    }
    
    // get current path setting
    $config = $this->config('my_empty_front_page.settings');
    $current = $config->get('empty_front_page_path');
    // get frontpage path setting
    $system_config = $this->config('system.site');
    $frontpage = $system_config->get('page.front');
    // current path may not be in use by the frontpage setting (change frontpage setting first)
    if($current == $frontpage && $current != '/node') {
      $form_state->setErrorByName('empty_path', t('The %token path cannot be changed because the current Default front page setting uses this path. Change the Site information setting for Default front page first!', ['%token' => $current]));
    }
  }
  
  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->config('my_empty_front_page.settings')
      ->set('empty_front_page_path', $form_state->getValue('empty_path'))
      ->save();
  	  
    \Drupal::service('router.builder')->rebuild();
    
    parent::submitForm($form, $form_state);
  }
}

In contrast to regular forms this class extends the ConfigFormBase class instead of the FormBase class. ConfigFormBase extends FormBase and introduces the use of the trait ConfigFormBaseTrait to the class. The abstract function getEditableConfigNames needs to be defined for the function config to return an editable configuration object. In our case it needs to return "my_empty_front_page.settings" in an array to be able to change the setting for the field "empty_front_page_path".

The function buildForm produces the form that contains a text field and a button. The text field "empty_field" is set to be required. The rest of of the validation for the text field is performed in the function validateForm. The validation ends with the prevention of changing the path when it is being used by the Site information setting by setting an error for the text field when this is the case.

The actual saving of the new setting is done in the function submitForm after which the router caching is rebuild to make the new value available for the dynamic route that we still need to define.

But first we need to make this form available my defining its route. For this we add the following to the file "my_empty_front_page.routing.yml" in the main module directory:

my_empty_front_page.admin_settings_form:
  path: '/admin/config/system/my_empty_front_page'
  defaults:
    _form: 'Drupal\my_empty_front_page\Form\MyEmptyFrontPageSettingsForm'
    _title: 'My Empty Front Page'
  requirements:
    _permission: 'administer my_empty_front_page'

Notice that this route contains a permission that we still need to define in the file "my_empty_front_page.permissions.yml" in the main module directory:

administer my_empty_front_page:
  title: 'Administer my empty front page'
  description: 'Allows users to administer my empty front page settings.'

After this we are ready to add the link to the Configuration menu by creating the file "my_empty_front_page.links.menu.yml" in the main module directory with the following content:

my_empty_front_page.settings:
  title: 'My Empty Front Page'
  description: 'Configure the path for the empty front page.'
  route_name: my_empty_front_page.admin_settings_form
  parent: system.admin_config_system

The parent to which we attach the menu link can be found in the file "system.links.menu.yml" of the core module System.

We have now made a menu link available that points to a form in which we can edit the empty path setting for our module. What remains to be done is to define a route to this path that we define in the empty path setting of the module. For this we need dynamic routing.

A dynamic route

When the file "my_empty_front_page.routing.yml" still contains the following code, it needs to be removed:

#my_empty_front_page.content:
#  path: '/empty'
#  defaults:
#    _controller: '\Drupal\my_empty_front_page\Controller\MyEmptyFrontPageController::emptyContent'
#  requirements:
#    _permission: 'access content'

Instead of a static route defined in a YAML file we are going to create a dynamic route by extending the event subscriber class RouteSubscriberBase in the subdirectory "module\custom\my_empty_front_page\src\Routing":

<?php

/**
 * @file
 * Contains \Drupal\my_empty_front_page\Routing\MyEmptyFrontPageRouting.
 */

namespace Drupal\my_empty_front_page\Routing;

use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

/**
 * Subscriber for My Empty Front page routes.
 */
class MyEmptyFrontPageRouting extends RouteSubscriberBase {
  /**
   * {@inheritdoc}
   */
  protected function alterRoutes(RouteCollection $collection) {
    $empty_page = \Drupal::config('my_empty_front_page.settings')->get('empty_front_page_path');
    $route = new Route($empty_page, 
      array(
        '_controller' =>'\Drupal\my_empty_front_page\Controller\MyEmptyFrontPageController::emptyContent',
      ), 
      array(
        '_permission' => 'access content',
      )
    );
    $collection->add('my_empty_front_page.content', $route);
  }
}

The function alterRoutes adds a new route to the route collection and uses the empty page path setting from the configuration "my_empty_front_page.settings". Now we still need to register the event subscriber as a service by adding the file "my_empty_front_page.services.yml" in the main module directory with the following content:

services:
  my_empty_front_page.subscriber:
    class: Drupal\my_empty_front_page\Routing\MyEmptyFrontPageRouting
    tags:
      - { name: event_subscriber }

For the sake of completeness I repeat the code that produces the empty content for the empty front page located in the file "MyEmptyFrontPageController.php" from an earlier blog that should be placed in the subdirectory "module\custom\my_empty_front_page\src\Controller":

<?php
namespace Drupal\my_empty_front_page\Controller;

use Drupal\Core\Controller\ControllerBase;

class MyEmptyFrontPageController extends ControllerBase {
  /**
   * Return empty content for an empty front page
   */
  public function emptyContent() {
    return array('#markup' => '');
  }
}

You can now install the module. Set the empty front page path in Configuration > System > My Empty Front Page and set the Default front page in Configuration > System > Site information to the same value.

Add new comment