Services and dependency injection in Drupal 8

Last updated on
12 February 2018

In Drupal 8 speak, a service is any object managed by the services container.

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 Some examples:

    class: Drupal\Core\Language\LanguageManager
    arguments: ['@language.default']
    class: Drupal\Core\Path\AliasManager
    arguments: ['@path.crud', '@path.alias_whitelist', '@language_manager']
    class: Drupal\Core\StringTranslation\TranslationManager
    class: Drupal\Core\Breadcrumb\BreadcrumbManager
    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. (The @ sign is needed to tell Drupal that the argument is a service. If we omitted the @ sign, the argument would be a simple string).

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 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.

Note: It's not possible to inject services to entity object. See this issue for more details.

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 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.

// Returns a Drupal\Core\Database\Connection object.
$connection = \Drupal::database();
$result = $connection->select('node', 'n')
  ->fields('n', array('nid'))

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

// Returns a Drupal\Core\Datetime\Date object.
$date = \Drupal::service('date');

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 3.4 documentation.

Defining your own services

You can define your own services using an file, where example is the name of the module defining the service. This file uses the same structure as the 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.