Drupal 9: An Introduction To Services And Dependency Injection

Drupal 8 and 9 are built upon services, with many parts of the system available through dependency injection, so it's important to understand the concepts. Services are a way to wrap objects and use dependency injection to produce a common interface. They are powerful and are used all over Drupal to do pretty much everything.

They can, however, be a little difficult for newcomers to the system to understand, especially if they are coming from Drupal 7 or other non-object oriented systems. When you look at some Drupal source code you are likely to see objects being created out of apparent thin air. It's a little hard to know where they come from if you aren't used to the how they work.

I first came across services when I started using Drupal 8 and it took me a little while to get my head around what they are and what they do. Before I understood them, I saw a lot of people online attempting to help by just pointing people to one service or another using this sort of construct.

$thing = \Drupal::service('thing');

This is helpful if you are familiar with Drupal services, but if you aren't then this doesn't tell you much. It is also bad practice to use this construct in certain situations, which I'll let into later on. If you have seen that construct around the internet but don't know what it means then I hope to clear things up a little.

I actually gave this article as a talk at DrupalCamp London 2018, but I have found myself referring to the slides quite often since then. I thought I would write it up as a couple of articles. Since I gave that talk around Drupal 8 I have updated the examples to be in line with Drupal 9.

Let's start with using Drupal services.

Using Drupal Services

The good news is that using Drupal services is pretty simple. Indeed, most of the complexity of services is deliberately hidden away from you. This allows you to get on with the work at hand without having to worry about where to get this or that object from and what parameters its constructor needs. 

There are many different services in Drupal, that govern everything. If you want access to configuration, the internal cron system, path and routing, the rendering process, translations, queues, cache and even date calculations then you can use a Drupal service to do that. I have just mentioned a handful here, but there are plenty more services available in Drupal 9

A good example of a service that is often used is the alias manager service. This service wraps the Drupal\path_alias\AliasManager class in Drupal and allows developers access to find an alias for a given path. This means that given a path like "/node/123" you can translate this to an alias in the form of "/page/some-page". This is useful if you have the node ID and want to find the correct path to the node so you can print it out. There are other ways to do this, especially if you have the full Node object, but this is used outside of that situation.

The service can be used the in following way. We use the \Drupal::service() method to get an instantiated AliasManager object and then use a function in that object to translate the path to the alias.

$aliasManager = \Drupal::service('path_alias.manager');
$path = '/node/123';
$alias = $aliasManager->getAliasByPath($path);

As the service returns an object we can chain together the method calls and do the alias lookup in one line, like this.

$path = '/node/123';
$alias = \Drupal::service('path_alias.manager')->getAliasByPath($path);

This does exactly the same thing as the above example, but in a single line of code

If you are writing code in Drupal it is also good practice to include docblock comments around this line so that your IDE can translate what type of object the $aliasManager variable contains.

/* @var \Drupal\path_alias\AliasManager $aliasManager */
$aliasManager = \Drupal::service('path_alias.manager');

When you start writing code your IDE will how print out a list of the methods you have access through, via the service object. Having this in place really helps you tap into the full functionality of the service and will absolutely speed up your development. This is an example of this working in PHPStorm.

Using docblock comments in PHPStorm to show the methods inside a Drupal service object.

Before you go off and start using this construct in all of your custom Drupal classes you should know that the above code should only really be used in static methods, hooks and preprocess functions in your theme. This is because you can use Drupal to inject services into your objects, whereas this isn't possible with static methods and stand alone functions. I will address this again later in the article.

Where To Find Services In Drupal

Services are all defined in YML files within Drupal. Every module that wants to define a service needs to create a file with the name of [module name].services.yml. This means that if we want to find services we just need to search the Drupal codebase within files that end in services.yml.

The path_alias.manager service I looked at earlier is defined in the file path_alias.services.yml, along with a few other alias based services. Since I have already shown how this works let's look at the service footprint.

The path_alias.manager service is defined in the following way.

  path_alias.manager:
    class: Drupal\path_alias\AliasManager
    arguments: ['@path_alias.repository', '@path_alias.whitelist', '@language_manager', '@cache.data']

The first line here is the name of the service and is used to ask Drupal to instantiate it, in this case the name is path_alias.manager.

The second line tells Drupal where to find the class it needs to instantiate. This points to a namespace of the class, rather than the filename, but it tells us that this class is in source directory of the path_alias module.

The final line consists of an array of four arguments. These arguments are an optional parameter that tell Drupal what arguments the constructor of the AliasManager requires. If we look at the constructor of the AliasManager class we can see that it requires four parameters, which map from the list of arguments to the parameters of the constructor.

class AliasManager implements AliasManagerInterface {
  /**
   * Constructs an AliasManager.
   *
   * @param \Drupal\path_alias\AliasRepositoryInterface $alias_repository
   *   The path alias repository.
   * @param \Drupal\path_alias\AliasWhitelistInterface $whitelist
   *   The whitelist implementation to use.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   The language manager.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
   *   Cache backend.
   */
  public function __construct($alias_repository, AliasWhitelistInterface $whitelist, LanguageManagerInterface $language_manager, CacheBackendInterface $cache) {
    $this->pathAliasRepository = $alias_repository;
    $this->languageManager = $language_manager;
    $this->whitelist = $whitelist;
    $this->cache = $cache;
  }
}

The @ symbol in the argument list above denotes that these arguments are other services. There are actually different types of arguments we can use here.

'@path_alias.repository' - This is a reference to another service. So in this case we are referencing the path_alias.repository, which is defined in the same services file. If you see this structure around the Drupal codebase you can find the service it referenced by searching for "path_alias.repository:" (i.e. with a trailing colon) in any services.yml file.

'%app.root%' - This is a configuration item. Some of these are set by Drupal internally (like app.root) but you can also inject configuration settings in this way. This tends to be used less often but it's an option if you want to inject a setting directly into the class.

'value' - This is a literal variable, so the a string of 'value' will be passed as an argument. You can also use numeric and boolean values here so you can pass values like 123 or true.

By knowing where to find services in Drupal you already know how to get access to the numerous different types of services that Drupal offers. There are a number of other options that are available when setting up a service class, but this is the minimum required. You should know that not all entries in services.yml files are pure services as there are a few other constructs that can be added to these files. You can create cache bins or event listeners though this interface and although they are created like services they shouldn't be created outside of Drupal's control.

Why Use Dependency Injection?

Dependency injection within Drupal is automated dependency injection. This means that with a few rules in a settings file we can create objects and have the dependencies automatically injected into it without having to create them manually. In the Drupal codebase, the Symfony component DependencyInjection manages the dependencies. If you use Symfony then you might find a lot of familiarity in how Drupal manages dependencies. 

But why use dependency injection? Couldn't we just create the objects we need and figure things out when we need them? Let's look at creating the AliasManager object without using any dependency injection.

Starting off with the AliasManager class, we know that it needs four parameters passed to it, so let's create that as a starting point where we create the AliasManager object.

use Drupal\path_alias\AliasManager;

$aliasManager = new AliasManager($alias_repository, $whitelist, $language_manager, $cache);

Of course, this doesn't work as we haven't defined any of the parameters, so start with the $alias_repository parameter. This is actually a reference to another service called path_alias.repository, which has it's own entry in the path_alias.services.yml file. The object we need to create here is called AliasRepository, so let's put the footprint of that object into the code.

use Drupal\path_alias\AliasManager;
use Drupal\path_alias\AliasRepository;

$alias_repository = new AliasRepository($connection)
$aliasManager = new AliasManager($alias_repository, $whitelist, $language_manager, $cache);

The AliasRepository constructor takes one parameter, which is a database connection. Thankfully, there exists a database factory that we can use to get a connection to the default database. Adding this to our code finishes the first parameter.

use Drupal\path_alias\AliasManager;
use Drupal\path_alias\AliasRepository;
use Drupal\Core\Database\Database;

$connection = Database::getConnection();
$alias_repository = new AliasRepository($connection);
$aliasManager = new AliasManager($alias_repository, $whitelist, $language_manager, $cache);

Moving onto the $whitelist parameter, this is also a service called path_alias.whitelist, also found in the path_alias.services.yml file. This service points to a class called AliasWhitelist. The constructor for this class takes 5 parameters. Adding the footprint of that object to our code we now have this.

use Drupal\path_alias\AliasManager;
use Drupal\path_alias\AliasRepository;
use Drupal\Core\Database\Database;
use Drupal\path_alias\AliasWhitelist;

$connection = Database::getConnection();
$alias_repository = new AliasRepository($connection);

$whitelist = new AliasWhitelist($cid, $cache, $lock, $state, $alias_repository);

$aliasManager = new AliasManager($alias_repository, $whitelist, $language_manager, $cache);

The next step is to start creating the other parameters for the AliasWhitelist object. The first is easy as this is just a string passed to the object to setup the cache identifier.

use Drupal\path_alias\AliasManager;
use Drupal\path_alias\AliasRepository;
use Drupal\Core\Database\Database;
use Drupal\path_alias\AliasWhitelist;

$connection = Database::getConnection();
$alias_repository = new AliasRepository($connection);

$cid = 'path_alias_whitelist';
$whitelist = new AliasWhitelist($cid, $cache, $lock, $state, $alias_repository);

$aliasManager = new AliasManager($alias_repository, $whitelist, $language_manager, $cache);

After this it starts getting complicated. The second parameter to the AliasWhitelist constructor is a Drupal cache object, which we can create using the built in CacheFactory object, which we also pass a couple of parameters to in order to create it.

use Drupal\path_alias\AliasManager;
use Drupal\path_alias\AliasRepository;
use Drupal\Core\Database\Database;
use Drupal\path_alias\AliasWhitelist;

$connection = Database::getConnection();
$alias_repository = new AliasRepository($connection);

$settings = Drupal\Core\Site\Settings::getInstance();
$default_bin_backends = $container->getParameter('cache_default_bin_backends');
$cacheFactory = new CacheFactory($settings, $default_bin_backends);

$cid = 'path_alias_whitelist';
$cache = $cacheFactory->get('bootstrap');
$lock = null;
$alias_repository = null;
$whitelist = new AliasWhitelist($cid, $cache, $lock, $state, $alias_repository);

$aliasManager = new AliasManager($alias_repository, $whitelist, $language_manager, $cache);

I'm already lost. I have written lots of code, I have more than 10 source code files open in my IDE, and I still haven't even finished creating the second parameter. There are another two parameters to create both of which have equally complex dependencies, and I haven't even gotten close to using the getAliasByPath() method.

What's worse is that I have already hard coded the database and configuration we are using as well as the configuration setup. If I go further I would need to also hard code other parameters and options into the code. Making these decisions means that it would be very hard to change this code in the future. If the AliasManager class changed in the future I would spend hours re-writing this code to make it work again. If this seems far fetched then remember that the path_alias.manager service used to be called path.alias_manager in Drupal 8, and this change also changed the underlying classes used by the service. 

Compare all of that complexity with using the dependency injection method. We would take more than 50 lines of code and reduce this down to just a single line.

$path = '/node/123';
$alias = \Drupal::service('path_alias.manager')->getAliasByPath($path);

This is far easier to read and understand and easily adaptable to changes to the underlying service without having to change our own code implementation. The example I have gone through here might seem convoluted, but I once showed all of this to a junior programmer who had been struggling to understand dependency injection. As soon as they saw the effect of not using dependency injection and all of the complexity involved they said that they understood why it was used. I wanted to include this example here as it really shows how services mask complexity.

Creating Custom Services With Injected Services

Whilst it is possible to use the \Drupal::service() construct wherever you need it, this shouldn't be used most of the time. Actually, it technically should only be used in static methods, hooks and theme functions where the flat function structure doesn't lend itself to dependency injection. Drupal allows you to inject it into the services into classes so that they are there and ready to use. When you are developing your own modules you will probably want to create your own services, which might have their own services being injected into them. The best way to show this in action is with a simple example.

To create a service we need to create a services.yml file. In the following example we are defining a custom service called mymodule.service_example that creates an object called ServiceExample, which will be created with another service called config.factory. The config.factory service is used to access the configuration of the Drupal site and is quite a common service to use.

services:
  mymodule.service_example:
    class: Drupal\mymodule\ServiceExample
    arguments: ['@config.factory']

Next, we need to create the ServiceExample class. It's best practice to create an interface that comes with your service, in the example this is ServiceExampleInterface. Using an interface allows you to follow proper SOLID principles by allowing other services that use this service to also accept different types of this class, which allows for better unit tests and a more versatile codebase.

The ServiceExample class just needs a constructor to accept the config.factory service and a class parameter to keep it in. When the object is created the config factory will automatically be injected into it, ready to use. I have added an example method called doThing() that makes use of the service.

<?php

namespace Drupal\mymodule;

use Drupal\Core\Config\ConfigFactoryInterface;

class ServiceExample implements ServiceExampleInterface {

  /**
   * The config name.
   *
   * @var string
   */
  protected $configName = 'mymodule.settings';

  /**
   * The config factory object.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * Constructs a ServiceExample object.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   A configuration factory instance.
   */
  public function __construct(ConfigFactoryInterface $config_factory) {
    $this->configFactory = $config_factory;
  }

  public function doThing() {
    $config = $this->configFactory->get($this->configName);
  }
}

To make use of this service you just need to create and use it like any other service. As an example we could use a hook_preprocess_block() to alter things within the block rendering system and use our new service to perform those modifications.

function mymodule_preprocess_block(&$variables) {
  \Drupal::service('mymodule.service_example')->doThing($variables);
}

This construct is pretty much the same for any service you want to inject. For example, you want to use the path_alias.manager service you just need to add the service to the modules services.yml file and then update the class to accept that new parameter. Once the service is created you can use the object just like normal.

Drupal Dependency Injection Interface

Controllers and Forms in Drupal are not defined through the services file and as a result they need a different mechanism to allow dependency injection to be used. In the case of Controllers and Forms the dependency injection is built right into the class and so it has a slightly different construct.

Controllers extend ControllerBase and Forms extend FormBase, both of which implement ContainerInjectionInterface. This interface needs to implement a static method called create(). This method is used to create the services that are needed by the class, which are then automatically injected into the object when it is created.

As an example, the following controller class called ExampleController injects the config.factory service using the create() dependency injection interface.

class ExampleController extends ControllerBase {

  /**
   * The config factory object.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

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

  /**
   * Constructs a ExampleController object.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   A configuration factory instance.
   */
  public function __construct(ConfigFactoryInterface $configFactory) {
    $this->configFactory = $configFactory;
  }
}

This controller is used in the normal way, we just need to define a route that uses this class in the module's routing.yml file. This calls a method in the class called page() that has access to all of the services that we have injected into the class using the create() construct.

mymodule_example_controller:
  path: '/example'
  defaults:
    _controller: '\Drupal\mymodule\Controller\ExampleController::page'
    _title: 'Example'
  requirements:
    _access: 'TRUE'

That's pretty much it, just remember that if the class you are extending implements ContainerInjectionInterface then it uses the create() method to do the dependency injection for the class. If not, then you need to define the services in your module services.yml file.

Conclusion

As I said at the start of this article, this can be a bit of a complex topic, especially for beginners to Drupal. Once you get your head around it, it becomes a really powerful tool and allows you to pull in different services without having to write lots of complex and fragile code. You can even override services and inject your own classes using ServiceProviderBase that I have talked about previously to poke holes in the Shield module. Just remember that services and their dependencies are defined in *.services.yml files and Controllers and Forms can have dependency injection built into them on creation.

Services and their automated dependency injection is intended to make your life as a developer easier so that you can concentrate on the code that matters to you.

You can read more about services in the Drupal documentation.

Comments

Very well written article. Small typo - book_preprocedd_block()

Permalink

Great article!

As a sidebar - I've often found the term "Dependency injection" intimidating. It has echoes of "SQL  injection" which is a very bad thing... Maybe a note to clarify that one is very good and one is very, very bad...

 

Some very small typos

"A good example of a service that is often used is the alias manager service. This service warps the Drupal\path_alias\AliasManager class in Drupal and allows developers access to find an alias for a given path."

I think this should be "wraps" , altho' "warps" almost makes sense to me:)

"After this is starts getting complicated"

Should be "it" ?

Cleaning up the internet one tiny little nit pick at a time!

Best

Ian

Permalink

Thank Ian, really appreciate you taking the time to both read and correct the article! :)

I have corrected those now.

Phil

Name
Philip Norton
Permalink

This is a really helpful article, thank you. One thing that seemed to have been passed over quickly is this note: “It's best practice to create an interface that comes with your service, in the example this is ServiceExampleInterface.” I was a little confused by this because I’m not familiar with defining an interface. Perhaps this article assumes you know how to do this? An example would be helpful.

Permalink

Hi Aaron, thanks for taking the time to read the article and for the comment!

So the best practice part comes from the use of interfaces to type hint arguments to methods, rather than to use class names. What this means is that it's better to enforce the type of object that would be passed by using interfaces, rather than enforce a concrete class being passed.

This is what's called "Dependency Inversion Principle", which is part of SOLID principles of software development, which I wrote about here -> https://www.hashbangcode.com/article/solid-principles-php.

For example, let's say we wanted to pass an argument to a method that used the ServiceExample object. If we used the concrete class we would write it like this.

public function someFunction(ServiceExample $object) {
}

This means that we must pass an object of type ServiceExample or a child of that class. We couldn't use an object that implements the interface since it must be an instance of ServiceExample class. Doing this can be restrictive in certain circumstances as it requires that the implementation of whatever ServiceExample is doing is always passed in.

By using interfaces we essentially relax this requirement so that anything that implements a ServiceExampleInterface can be passed to the method. This can be any type of object, as long as it implements the interface.

public function someFunction(ServiceExampleInterface $object) {
}

This gives us the ability to create different flavours of the interface without having to overwrite code from parent classes. It also makes testing easier since we can create simple testing classes (or mocking classes) that implement the interface without having to override any code from the concrete ServiceExample class.

Let me know if that's any clearer. You might not see any benefits of using interfaces to start with, but you will absolutely encounter a problem at some point when using concrete classes as parameters.

Name
Philip Norton
Permalink

First of all thanks taking your time and write this amazing article.
Hands down the best services documentation i have seen.
I have tried to learn about services from drupal.org, but the most confusing part of the service was cleared through this documentation.
 

Permalink

Thank you so much Kaustab. Your feedback is greatly appreciated :)

Name
Philip Norton
Permalink

Thank you for this article. I am coming from Drupal 7 and It was difficult to understand this until now. Very good documentation and I have a clear idea how the services work.

Please keep it up!

Permalink

Really happy you found it useful sandymend o7

Name
Philip Norton
Permalink

This is the best explanation of Services and Dependency Injection in the web. Bravo. I will share it with my colleagues.

Permalink

Thanks very much for your kind words Dan, and the coffee! Very much appreciated :)

Name
Philip Norton
Permalink

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
5 + 10 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.