You are here

Drupal 8: Hello, Configuration Management

Submitted by mtift on Tue, 08/27/2013 - 12:40
pick your hello world case

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: 
  path: '/hello' 
  defaults: 
    _content: '\Drupal\hello\Controller\HelloController::content' 
  requirements: 
    _permission: 'access content'

hello.settings_form:
  path: '/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\Form\ConfigFormBase;
use Symfony\Component\DependencyInjection\ContainerInterface;

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

  /**
   * 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.
   */
  public function __construct(ConfigFactory $config_factory, ContextInterface $context) {
    parent::__construct($config_factory, $context);
  }

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

  /**
   * {@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.

Tags:

Comments

A really good one. Thanks for that post!

I have on issue though. As long as the configuration form isn't submitted at least once and the hello.config.yml hasn't been created as sites/default/files/config_XX/active/hello.settings.yml the HelloController class fails to load the hello.config.yml from config/hello.settings.yml.

Therefore the $markup variable (the default one) cannot be set in the HelloController class and visiting sitename/hello results in an error message:
Notice: Undefined variable: markup in Drupal\hello\Controller\HelloController->content() (line 19 of modules/custom/hello/lib/Drupal/hello/Controller/HelloController.php).

Any ideas?

Is hello_settings the same as hello.settings, or have I missed a mapping somewhere. If they are they same, why not consistently use underscores or periods?

There also seems to be much more boilerplate code than necessary. Have there been any efforts to reduce that, e.g. by providing a standard implementation or moving more code to SystemConfigFormBase?

PS This form offers to notify me when new comments are posted, but doesn't ask for my email address.

Thanks for your questions! Sorry about the confusion. hello_settings and hello.settings were unrelated. I changed the route name and getFormID in the code to help make the distinction more clear.

And now you should be able to leave your contact information to subscribe.

В рамках нашей интереса komunal-stroy.ru
вступает: комплексный реставрация и отделка зданий, квартир и рабочих помещений.
Мы предлагаем, важные стандарты производительности и предоставления
соответствуюшего функционирования в монтаже.

Наладка
установка канализации (туалет,
умывальник, монтаж лотков, ванны);
монтаж отопительных устройств (распределение тепла, вентиляции, усовершенствование,
рекуперация);
гипервентиляция и кондиционирование воздуха.

Отделочные работы
облицовку (плитка - плитка, напольная плитка,
гранит и т.д.);
изоляцию фасадов;
мощение;
земляные работы и снос;
парк;
изделия из железа - заборы, перила,
кованые элементы и др.
стальные устройства;
деревянных конструкций;
железобетонных конструкций.
На основе полного познания каждый гость в силах подобрать что-то для себя.
Самое важное, что всегда получаете достоверную подлинную информацию,
из авторитетного источника.
Их получают на основе не обупликованных, уникальных документов и в сочетании со спецификациями ремонтно-строительного
материала.

This example sidesteps configuration schemas (and translatability / language handling) by including a set of predefined keys as options and no freeform text. In reality for any configuration that may use freeform text, defining a schema is important. Then the schema may be used to generate forms and possibly avoid much of the form code. There should be a form generation utility but that is not yet available. I don't think core will provide this, maybe some contrib, eg. ctools? This overall may increase your declarative definition but decrease the need to write your own form code.

Correct me if i am wrong, but shouldnt HelloConfigForm::__construct receive a ContextInterface rather than a ModuleHandler ?

It works without it, but you are correct. I've updated the code. Thanks for catching that!

I guess because SystemConfigFormBase already implements the constructor injection with ContainerInterface and ConfigFactory, you don't really need to include it in your settings form class, right?

On an out-of-the-box drupal-8.0-alpha3 install:

[13-Sep-2013 17:30:56 America/Chicago] PHP Fatal error: Call to undefined method Drupal\Core\Extension\CachedModuleHandler::init() in /nope/Sites/mamp/drupal8-alpha3/core/lib/Drupal/Core/Config/ConfigFactory.php on line 194

Sorry about the errors. Try it now. I made a couple of changes to HelloConfigForm.php.

mtift - I'm trying to wrap my head around the way this actually works rather than just copying and pasting it. I could write out what I'm thinking, but at the risk of being horribly wrong. Instead, can you explain the basic workflow that is happening here regarding the usage of __construct, create, ConfigFactory, ContextInterface and ModuleHandler?

e.g., what does ModuleHandler do? what is the create function actually doing?

I'm not looking for extreme behind-the-scenes technical detail.. I guess I'm just wanting to "learn to multiply, instead of learning my multiplication tables".

I'm chasing Drupal 8 head a bit to keep this module working, but I don't think I should keep it up for much longer because the text is going to start getting confusing. Here are some changes to D8 the past couple of days that have necessitated changes to this module:

https://drupal.org/node/2089731
https://drupal.org/node/2089727

Josh, thanks for your continued interest. I've also removed the ModuleHandler code. For some of your other questions, I'd suggests you check out the WSCCI Conversion Guide.

In the coming weeks I am planning to update the Configuration System documentation. We have been holding off on this for a bit because we are continuing to find bugs and fix them. For now, here are some places where you can find some additional information:

https://drupal.org/node/1667894
https://drupal.org/node/1809490

I'll add a comment here when I, or someone else, updates the documentation on drupal.org.