Building a Drupal 8 Module – Configuration Management and the Service Container

Daniel Sipos
Tweet
This entry is part 3 of 5 in the series How to Build a Drupal 8 Module

How to Build a Drupal 8 Module

In the previous article on Drupal 8 module development, we’ve looked at creating block types and forms. We’ve seen that blocks are now reusable and how everything we need to do for defining block types happens in one single class. Similarly, form generation functions are also grouped under one class with specific methods performing tasks similar to what we are used to in Drupal 7.

In this tutorial, I will continue where we left off. I will illustrate how we can turn our DemoForm into a form used to store a value through the Drupal 8 configuration system. Following that, we will talk a bit about the service container and dependency injection by way of illustration.

Don’t forget that you can check out this repository if you want to get all the code we write in this tutorial series.

Configuration forms

When we first defined our DemoForm, we extended the FormBase class which is the simplest implementation of the FormInterface. However, Drupal 8 also comes with a ConfigFormBase that provides some additional functionality which makes it very easy to interact with the configuration system.

What we will do now is transform DemoForm into one which will be used to store the email address the user enters. The first thing we should do is replace the extended class with ConfigFormBase (and of course use it):

use Drupal\Core\Form\ConfigFormBase;

class DemoForm extends ConfigFormBase {

Before we move on to changing other things in the form, let’s understand a bit how simple configuration works in Drupal 8. I say simple because there are also configuration entities that are more complex and that we will not cover today. As it stands now, configuration provided by modules (core or contrib) is stored in YAML files. On enabling a module, this data gets imported into the database (for better performance while working with it). Through the UI we can change this configuration which is then easily exportable to YAML files for deployment across different sites.

A module can provide default configuration in a YAML file located in the config/install folder in the module root directory. The convention for naming this file is to prefix it with the name of the module. So let’s create one called demo.settings.yml. Inside this file, let’s paste the following:

demo:
  email_address: demo@demo.com

This is a nested structure (like an associative array in PHP). Under the key demo, we have another key|value pair. And usually to access these nested values we use a dot(.). In our case demo.email_address.

Once we have this file in place, an important thing you need to remember is that this file gets imported only when the module is installed. So go ahead and reinstall it. And now we can turn back to our form and go through the methods that need adapting one by one.

This is how the buildForm() method should look like now:

public function buildForm(array $form, array &$form_state) {
  
  $form = parent::buildForm($form, $form_state);
  
  $config = $this->config('demo.settings');
  
  $form['email'] = array(
    '#type' => 'email',
    '#title' => $this->t('Your .com email address.'),
    '#default_value' => $config->get('demo.email_address')
  );
  
  return $form;
}

First of all, as opposed to FormBase, the ConfigFormBase class implements this method as well in order to add elements to the form array (a submit button). So we can use what the parent did before adding our own elements.

Now for the configuration part. Drupal 8 provides a Config object that we can use to interact with the configuration. Some classes already have it available through dependency injection. ConfigFormBase is one such class.

As you can see, we are using the config() method of the parent class to retrieve a Config object populated with our demo.settings simple configuration. Then, for the #default_value of the email form element, we use the get() method of the Config object to retrieve the value of the email address.

Next, we only need to change the submit handler because the validateForm() method can stay the same for now:

public function submitForm(array &$form, array &$form_state) {
  
  $config = $this->config('demo.settings');
  $config->set('demo.email_address', $form_state['values']['email']);
  $config->save();
  
  return parent::submitForm($form, $form_state);
}

In this method we first retrieve the Config object for our configuration (like we did before). Then, we use its set() method to change the value of the email_address to the value the user submitted. Then we use the save() method to save the configuration. Lastly, we extend the parent submit handler because it does contain some functionality (in this case it sets a Drupal message to the screen).

And that’s pretty much it. You can clear the cache and try it out. By submitting a new email address, you are storing it in the configuration. The module demo.settings.yml file won’t change of course, but you can go and export the demo.settings configuration and import it into another site.

The service container and dependency injection

The next thing we are going to look at is the service container. The idea behind services is to split functionality into reusable components. Therefore a service is a PHP class that performs some global operations and that is registered with the service container in order to be accessed.

Dependency injection is the way through which we pass objects to other objects in order to ensure decoupling. Each service needs to deal with one thing and if it needs another service, the latter can be injected into the former. But we’ll see how in a minute.

Going forward, we will create a very simple service and register it with the container. It will only have one real method that returns a simple value. Then, we will inject that service as a dependency to our DemoController and make use of the value provided by the service.

In order to register a service, we need to create a demo.services.yml file located in the root of our module, with the following contents:

services:
    demo.demo_service:
        class: Drupal\demo\DemoService

The file naming convention is module_name.services.yml.

The first line creates an array of services. The second line defines the first service (called demo_service, prefixed by the module name). The third line specifies the class that will be instantiated for this service. It follows to create the DemoService.php class file in the src/ folder of our module. This is what my service does (nothing really, it’s just to illustrate how to use it):

<?php

/**
 * @file
 * Contains Drupal\demo\DemoService.
 */

namespace Drupal\demo;

class DemoService {
  
  protected $demo_value;
  
  public function __construct() {
    $this->demo_value = 'Upchuk';
  }
  
  public function getDemoValue() {
    return $this->demo_value;
  }
  
}

No need to explain anything here as it’s very basic. Next, let’s turn to our DemoController and use this service. There are two ways we can do this: accessing the container globally through the \Drupal class or use dependency injection to pass an object of this class to our controller. Best practice says we should do it the second way, so that’s what we’ll do. But sometimes you will need to access a service globally. For that, you can do something like this:

$service = \Drupal::service('demo.demo_service');

And now $service is an object of the class DemoService we just created. But let’s see how to inject our service in the DemoController class as a dependency. I will explain first what needs to be done, then you’ll see the entire controller with all the changes made to it.

First, we need access to the service container. With controllers, this is really easy. We can extend the ControllerBase class which gives us that in addition to some other helpers. Alternatively, our Controller can implement the ContainerInjectionInterface that also gives us access to the container. But we’ll stick to ControllerBase so we’ll need to use that class.

Next, we need to also use the Symfony 2 ContainerInterface as a requirement of the create() method that instantiates another object of our controller class and passes to it the services we want.

Finally, we’ll need a constructor to get the passed service objects (the ones that create() returns) and assign them to properties for later use. The order in which the objects are returned by the create() method needs to be reflected in the order they are passed to the constructor.

So let’s see our revised DemoController:

<?php

/**
 * @file
 * Contains \Drupal\demo\Controller\DemoController.
 */

namespace Drupal\demo\Controller;

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

/**
 * DemoController.
 */
class DemoController extends ControllerBase {
  
  protected $demoService;
  
  /**
   * Class constructor.
   */
  public function __construct($demoService) {
    $this->demoService = $demoService;
  }
  
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('demo.demo_service')
    );
  }
  
  /**
   * Generates an example page.
   */
  public function demo() {
    return array(
      '#markup' => t('Hello @value!', array('@value' => $this->demoService->getDemoValue())),
    );
  }
}

As you can see, all the steps are there. The create() method creates a new instance of our controller class passing to it our service retrieved from the container. And in the end, an instance of the DemoService class gets stored in the $demoService property, and we can use it to call its getDemoValue() method. And this value is then used in the Hello message. Clear your cache and give it a try. Go to the demo/ path and you should see Hello Upchuk! printed on the page.

I’m sure you can see the power of the service container as we can now write decoupled functionality and pass it where it’s needed. I did not show you how, but you can also declare dependencies when you register services. This means that when Drupal instantiates a service object, it will do so for all its dependencies as well, and pass them to its constructor. You can read more about how to do that on this documentation page.

Conclusion

In this article we’ve looked at a lot of cool stuff. We’ve seen how the configuration system manages simple configuration and what we have available form-wise for this. I do encourage you to explore how the ConfigFormBase is implemented and what you have available if you extend it. Additionally, you should play around in the UI with importing/exporting configuration between sites. This will be a great improvement for the deployment process from now on.

Then, we looked at services, what they are and how they work. A great way of maintaining reusable and decoupled pieces of functionality accessible from anywhere. And I do hope the concept of dependency injection is no longer so scary (if it was for you). It is basically the equivalent of passing parameters to procedural functions, but done using constructor methods (or setters), under the hood, by Symfony and its great service container.

How to Build a Drupal 8 Module

<< Building a Drupal 8 Module: Blocks and FormsThe Drupal 8 version of EntityFieldQuery >>

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Mohd. Mahabubul ALam

    Services and dependency injection in Drupal 8

    Drupal
    8 introduces the concept of services to decouple reusable functionality
    and makes these services pluggable and replaceable by registering them
    with a service container. As a developer, it is best practice to access
    any of the services provided by Drupal via the service container to
    ensure the decoupled nature of these systems is respected. The Symfony 2 documentation has a great introduction to services.

    As a developer, services are used to perform operations like
    accessing the database or sending an e-mail. Rather than use PHP’s
    native MySQL functions, we use the core-provided service via the service
    container to perform this operation so that our code can simply access
    the database without having to worry about whether the database is MySQL
    or SQLlite, or if the mechanism for sending e-mail is SMTP or something
    else.

    Core services

    Core services are defined in CoreServiceProvider.php and core.services.yml. Some examples:


    language_manager:
    class: DrupalCoreLanguageLanguageManager
    arguments: ['@language.default']

    path.alias_manager:
    class: DrupalCorePathAliasManager
    arguments: ['@path.crud', '@path.alias_whitelist', '@language_manager']

    string_translation:
    class: DrupalCoreStringTranslationTranslationManager

    breadcrumb:
    class: DrupalCoreBreadcrumbBreadcrumbManager
    arguments: ['@module_handler']

    Each service can also depend on other services. In the example above, the path.alias_manager is dependent on the path.crud, path.alias_whitelist and language_manager services specified in the arguments list. Define a dependency on a service by prefixing the name of the dependee service with an @ sign, like @language_manager. When code elsewhere in Drupal requests the path.alias_manager service, the service container ensures that the path.crud, path.alias_whitelist and language_manager services are passed to the constructor of the path.alias_manager service by first requesting each of those and then passing them in turn to the constructor of the path.alias_manager service. In turn the language_manager depends on the language.default, etc.

    Drupal 8 contains a large number of services and the best way to get a list of those that are available is by looking at the CoreServiceProvider.php and core.services.yml files.

    A service container (or dependency injection container) is a
    PHP object that manages the instantiation of services. Drupal’s service
    container is built on top of the Symfony 2 service container and
    documentation on the structure of this file, special characters,
    optional dependencies, etc. can all be found in the Symfony 2 service container documentation.

    Accessing services in objects using dependency injection

    Dependency injection is the preferred method for accessing
    and using services in Drupal 8 and should be used whenever possible.
    Rather than calling out to the global services container, services are
    instead passed as arguments to a constructor or injected via setter
    methods. Many of the controller and plugin classes provided by modules
    in core make use of this pattern and serve as a good resource for seeing
    it in action.

    The global Drupal class is to be used within global functions.
    However, Drupal 8’s base approach revolves around classes in the form of
    controllers, plugins, and so on. The best practice for these is not to call out to the global service container
    and instead pass in the required services as arguments to a constructor
    or inject the needed services via service setter methods.

    Passing in the services an object depends on explicitly is called
    dependency injection. In several cases, dependencies are passed
    explicitly in constructors. For example, route access checkers get the current user injected in service creation and the current request passed on when checking access. You can also use setter methods to set a dependency.

    Accessing services in global functions

    The global Drupal class provides static methods to access several of the most common services. For example, Drupal::moduleHandler() will get return the module handler service or Drupal::translation() will return the string translation service. If there is no dedicated method for the service you want to use, you can use the Drupal::service() method to retrieve any defined service.

    Example: Accessing the database service via a dedicated Drupal::database() accessor.

    select(‘node’, ‘n’)
    ->fields(‘n’, array(‘nid’))
    ->execute();
    ?>

    Example: Accessing the date service via the generic Drupal::service() method.

    Ideally, you should minimize the code sitting in global functions and
    refactor to be on controllers, listeners, plugins, etc. as appropriate,
    where actual dependencies are injected; see below.

    Both have code examples in the Symfony 2 documentation.

    Defining your own services

    You can define your own services using an example.services.yml file, where example is the name of the module defining the service. This file uses the same structure as the core.services.yml file.

    There are several subsystems requiring you to define services. For example, custom route access checker classes, custom parameter upcasting, or defining a plugin manager all require you to register your class as a service.

    It is also possible to add more YAML files to discover services by using $GLOBALS['conf']['container_yamls']. The use of that should be very rare though.

    Comparing Drupal 7 global functions to Drupal 8 services

    Let’s take a look at the code required to invoke a module’s hook as
    an example of the differences between Drupal 7 and 8. In Drupal 7, you
    would use module_invoke_all(‘help’) to invoke all hook_help() implementations. Because we’re calling the module_invoke_all()
    function directly in our code, there is no easy way for someone to
    modify the way Drupal invokes modules without making changes to the core
    function.

    In Drupal 8, the module_* functions are replaced by the ModuleHandler service. So in Drupal 8 you would use Drupal::moduleHandler()->invokeAll(‘help’). In this example, Drupal::moduleHandler() locates the registered implementation of the module handler service in via the service container and then calls the invokeAll() method on that service.

    This approach is better than the Drupal 7 solution because it allows a
    Drupal distribution or hosting provider or another module to override
    the way invoking modules works by changing the class registered for the
    module handler service with another that implements the
    ModuleHandlerInterface. The change is transparent for the rest of the
    Drupal code. This means more parts of Drupal can be swapped out without
    hacking core. The dependencies of code are also better documented and
    the borders of concern better separated. Finally, the services can be
    unit tested using their interface with more compact and quicker tests
    compared to integration tests.

    Comparing Drupal 7 global variables vs. Drupal 8 services

    Several Drupal 7 global values like global $language and global $user are also now accessed via services in Drupal 8 (and not global variables). See Drupal::languageManager()->getLanguage(Language::TYPE_INTERFACE) and Drupal::currentUser() respectively.