Drupal 8: Hello, Configuration Management

choose case
PLEASE NOTE: The information in this post is now out-of-date. For now, you can find some additional information in the Configuration System section on Drupal.org

If you have not read Alex Bronstein's (effulgentsia's) excellent blog post about creating your first Drupal 8 module, go read it: Drupal 8: Hello OOP, Hello world! This post will expand on Alex's discussion and introduce the new configuration API in Drupal 8. Here we will have the modest goal of making the display of the text "Hello, World!" configurable -- more specifically, we will give site administrators the ability to make the text UPPERCASE or Title Case. I can tell you are excited.

Fortunately, the .info.yml file does not need to change:

hello.info.yml

name: Hello 
core: 8.x 
type: module 

We need to add some code to the routing file, and it will follow a similar organization as before:

hello.routing.yml

hello: 
  pattern: '/hello' 
  defaults: 
    _content: '\Drupal\hello\Controller\HelloController::content' 
  requirements: 
    _permission: 'access content'

hello.settings_form:
  pattern: '/hello/config'
  defaults:
    _form: 'Drupal\hello\Form\HelloConfigForm'
  requirements:
    _permission: 'administer site configuration'

Notice that instead of adding _content, this route adds a _form so that when a visitor goes to hello/config, the HelloConfigForm will get called. So far so good.

The .module file also should look rather familiar:

hello.module

/**
 * Implements hook_menu().
 */
function hello_menu() {
  $items['hello'] = array(
    'title' => 'Hello',
    'route_name' => 'hello',
  );
  $items['hello/config'] = array(
    'title' => 'Hello Configuration',
    'route_name' => 'hello.settings_form',
  );
  return $items;
}

The second of the two $items connects the URL hello/config with the route hello.settings_form, which will appear again in the next few code examples. Still seems fairly straightforward.

When the Hello module is installed, we want to have a default case setting configured, so let's make the text title case by default.

config/hello.settings.yml

case: title

Out of the box, Drupal's new configuration system uses two directories for configuration: active and staging. When the Hello module is enabled, the configuration system will look in the Hello module's config directory and find hello.settings.yml. Then it will create a new file in the "active" configuration directory, also named hello.settings.yml, with the same single line that reads case: title. The default location for the active and staging directories is sites/default/files/config_xxx, with xxx being a hash that is unique to each Drupal installation for security purposes (security options were discussed at length in this thread: https://groups.drupal.org/node/155559).

Although we could have accomplished the same thing in an install file by implementing hook_install() and calling Drupal::config('hello.settings')->set('case', 'title')->save();, the preferred -- and easier -- method is to provide default configuration for the module in a config directory (as we have done).

The HelloController class needs to be altered so that it will display the correct case:

lib/Drupal/hello/Controller/HelloController.php

namespace Drupal\hello\Controller;

use Drupal\Core\Controller\ControllerBase;

class HelloController extends ControllerBase {
  public function content() {
    $config = $this->config('hello.settings');
    $case = $config->get('case');
    if ($case == 'upper') {
      $markup = $this->t('HELLO, WORLD!');
    }
    elseif ($case == 'title') {
      $markup = $this->t('Hello, World!');
    }

    return array(
      '#markup' => $markup,
    );
  }
}

While hello.install interacted with the configuration system using the Drupal class, calling Drupal::config(), the Drupal class exists only to support legacy code. In HelloController we take another shortcut of sorts by extending a fairly recent addition to Drupal core, ControllerBase. This class was created to "stop the boilerplate code madness" by providing access to a number of utility methods, including config(). This class should only be used for "trivial glue code," and I think our Hello, World! module qualifies.

To give site administrators the ability to change the display from title case to uppercase, we will use a form. Much like putting the controller in lib/Drupal/hello/Controller/HelloController.php, the code for the configuration form goes in lib/Drupal/hello/Form/HelloConfigForm.php. It's not a requirement, but it is a convention to put forms in a Form directory and use the word Form in the filename. Here is the code for the form:

lib/Drupal/hello/Form/HelloConfigForm.php

/**
 * @file
 * Contains \Drupal\hello\Form\HelloConfigForm.
 */

namespace Drupal\hello\Form;

use Drupal\Core\Config\ConfigFactory;
use Drupal\Core\Config\Context\ContextInterface;
use Drupal\Core\Extension\ModuleHandler;
use Drupal\system\SystemConfigFormBase;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Configure text display settings for this the hello world page.
 */
class HelloConfigForm extends SystemConfigFormBase {

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandler
   */
  protected $moduleHandler;

  /**
   * Constructs a \Drupal\hello\Form\HelloConfigForm object.
   *
   * @param \Drupal\Core\Config\ConfigFactory $config_factory
   *   The factory for configuration objects.
   * @param \Drupal\Core\Config\Context\ContextInterface $context
   *   The configuration context to use.
   * @param \Drupal\Core\Extension\ModuleHandler $module_handler
   *   The module handler.
   */
  public function __construct(ConfigFactory $config_factory, ContextInterface $context, ModuleHandler $module_handler) {
    parent::__construct($config_factory, $context);
    $this->moduleHandler = $module_handler;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('config.factory'),
      $container->get('config.context.free'),
      $container->get('module_handler')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getFormID() {
    return 'hello.settings_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, array &$form_state) {
    $config = $this->configFactory->get('hello.settings');
    $case = $config->get('case');
    $form['hello_case'] = array(
      '#type' => 'radios',
      '#title' => $this->t('Configure Hello World Text'),
      '#default_value' => $case,
      '#options' => array(
        'upper' => $this->t('UPPER'),
        'title' => $this->t('Title'),
      ),
      '#description' => $this->t('Choose the case of your "Hello, world!" message.'),
    );

    return parent::buildForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, array &$form_state) {
    $this->configFactory->get('hello.settings')
      ->set('case', $form_state['values']['hello_case'])
      ->save();

    parent::submitForm($form, $form_state);
  }
}

This form contains what was previously described as "boilerplate madness" and illustrates the preferred way of interacting with Drupal's configuration system: via dependency injection. The "boilerplate" parts include the use statements near the top, the __construct() method, and the create() method.

Dependency injection is the design pattern that all of the cool kids use. It allows developers the ability to standardize and centralize the way objects are constructed. If you want to read more about dependency injection, check out Crell's WSCCI Conversion Guide or this change record. If you want to watch a presentation about it, check out Kat Bailey's talk Dependency Injection in Drupal 8. If you want to explain it to a five year old, read this stackoverflow answer.

For the HelloConfigForm, dependency injection enables code like this: $config = $this->configFactory->get('hello.settings'). It's a lot more code than writing Drupal::config() or ControllerBase::config() -- and we could have dropped three of the four use statements and omitted two of the methods -- but I assure you that dependency injection is the preferred method.

The rest of the code should look rather familiar if you already know about writing modules with Drupal 7. You can read more about the {@inheritdoc} in this issue, but the basic idea is that it is less code. (If you want to read more about forms in Drupal 8, check out Drupal 8: Forms, OOP style).

This may seem like a fair amount of code just to use the configuration system, but in fact this Hello module takes advantage of multiple new features offered by Drupal 8. The meat of this process could be reduced to four steps, and only the last two access configuration:

  1. Implement hook_menu()
  2. Add a route in hello.routing.yml
  3. Create a controller to display the text
  4. Create a configuration form

Of course the most important part of this code is that we're using Drupal's new configuration system. This Hello module stores its configuration data in a configuration file (and in the database in the cache_config table), and each time the form is changed sites/default/files/config_xxx/active/hello.settings.yml is updated, thus allowing the ability to transfer configuration between sites. That, however, is a topic for another post.

Until then, you can come to this site to learn more about the Configuration Management Initiative, find links to documentation, issues that need work, and much more. If you would like to get involved, the issues that are the most important for Configuration Management are listed on http://drupal8cmi.org/focus.

 

ACKNOWLEDGEMENTS

A special thanks to tim.plunkett, xjm, and effulgentsia, who each read an earlier draft of this post and helped me improve it.

I originally wrote this blog post for the Drupal 8 Configuration Management Initiative (CMI) site. I'm cross-posting it to my blog.

Tags: