Building a Drupal 8 Module- Config and the Service Container

Share this article

Please be aware that due to the development process Drupal 8 has been undergoing at the time of writing, some parts of the code might be outdated. Take a look at this repository in which I try to update the example code and make it work with the latest Drupal 8 release.

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.

Frequently Asked Questions about Building a Drupal 8 Module: Configuration Management and Service Container

What is the role of the service container in Drupal 8?

The service container in Drupal 8 is a crucial component that manages the creation of services, which are objects used globally across a Drupal application. It ensures that each service is instantiated only once, thereby saving memory and improving performance. The service container also handles dependency injection, which is a design pattern that allows one object to supply the dependencies of another object. This makes the code more modular, easier to test, and promotes better organization.

How do I define a new service in Drupal 8?

To define a new service in Drupal 8, you need to create a services.yml file in your module’s root directory. This file should contain the service’s name, class, and arguments. The class should be the fully qualified name of the class that implements the service, and the arguments should be any services or parameters that the service depends on.

What is the purpose of the Configuration Management in Drupal 8?

Configuration Management in Drupal 8 is a system that allows you to manage site configuration data in a consistent manner. It enables you to import, export, and synchronize configuration data, which can be useful when moving configuration changes from a development environment to a production site. It also provides a way to track and manage changes to your site’s configuration over time.

How can I use the Configuration Management system to export and import configuration data?

To export configuration data in Drupal 8, you can use the configuration management interface in the admin panel or use Drush commands. The exported data will be in YAML format, which can be easily read and edited. To import configuration data, you can upload the exported YAML files through the configuration management interface or use Drush commands. Remember to backup your site before importing configuration data to prevent any potential data loss.

What is Dependency Injection and why is it important in Drupal 8?

Dependency Injection is a design pattern that allows an object to supply the dependencies of another object. In Drupal 8, it’s used to make services and controllers more modular and testable. Instead of creating dependencies inside the object, they are passed in (injected) through the constructor or setter methods. This makes the code easier to test, more flexible, and less coupled.

How can I inject dependencies into my service in Drupal 8?

To inject dependencies into your service in Drupal 8, you need to define them in your service’s definition in the services.yml file. The dependencies should be listed under the ‘arguments’ key. When your service is created, the service container will automatically pass these dependencies to your service’s constructor.

What is the difference between a service and a plugin in Drupal 8?

In Drupal 8, a service is an object that performs global tasks across the application, while a plugin is an object that performs specific tasks in a pluggable manner. Services are defined in the services.yml file and are managed by the service container, while plugins are discovered and instantiated by the plugin manager.

How can I override a service in Drupal 8?

To override a service in Drupal 8, you need to define a new service with the same name as the service you want to override in your module’s services.yml file. Your new service should extend the original service’s class and override the methods you want to change.

How can I use the Configuration Management system to track changes to my site’s configuration?

The Configuration Management system in Drupal 8 provides a way to track changes to your site’s configuration through the configuration snapshot system. This system takes a snapshot of your site’s active configuration whenever you import or synchronize configuration data. You can then compare these snapshots to see what changes have been made.

What is the role of the services.yml file in Drupal 8?

The services.yml file in Drupal 8 is where you define your module’s services. Each service is defined with a unique name, the fully qualified name of the class that implements the service, and any services or parameters that the service depends on. The services.yml file is read by the service container, which manages the creation and injection of services.

Daniel SiposDaniel Sipos
View Author

Daniel Sipos is a Drupal developer who lives in Brussels, Belgium. He works professionally with Drupal but likes to use other PHP frameworks and technologies as well. He runs webomelette.com, a Drupal blog where he writes articles and tutorials about Drupal development, theming and site building.

cmsconfigurationdependencydependency injectiondidrupaldrupal 8drupal-planetFrameworksOOPHPPHPservice containersymfony
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week