Services and dependency injection in Drupal

Last updated on
5 February 2024

This documentation needs review. See "Help improve this page" in the sidebar.

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

Drupal 8 introduced the concept of services to decouple reusable functionality and makes these services pluggable and replaceable by registering them with a service container.

Another important benefit of dependency injection is that code will be easier to test via PHPUnit tests, because your domain's business logic will be separated from the huge amount of Drupal dependencies. Dependency injection makes testing your unique business logic much easier and reduces the processing time. The official PHPUnit documentation teaches you how to write tests for your unique business logic.

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 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: Drupal\Core\Language\LanguageManager
    arguments: ['@language.default']
  ...
  path.alias_manager:
    class: Drupal\Core\Path\AliasManager
    arguments: ['@path_alias.repository', '@path_alias.whitelist', '@language_manager', '@cache.data']
    deprecated: 'The "%service_id%" service is deprecated. Use "path_alias.manager" instead. See https://drupal.org/node/3092086'
  ...
  string_translation:
    class: Drupal\Core\StringTranslation\TranslationManager
  ...
  breadcrumb:
    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_alias.repository, path_alias.whitelist, cache.data 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_alias.repository, path_alias.whitelist, cache.data 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. The best way to get a list of the available services is by looking at the CoreServiceProvider.php and core.services.yml files. The Devel module provides a searchable list at /devel/container/service; Drush gives that list with drush devel:services which is equivalent to drupal debug:container in Drupal Console.

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 service container. Documentation on the structure of this file, special characters, optional dependencies, etc. can all be found in the Symfony service container documentation.

Accessing services

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.

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.

Explicitly passing in the services that an object depends on is called dependency injection. In several cases, dependencies are passed explicitly in class 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.

Using 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'))
  ->execute();

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

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

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

Defining services by fully qualified name of PHP namespace

It's not needed to specify a machine name as a name for your service. Instead you can simply use the PHP class namespace:

services:
  Drupal\coffee_shop\Service\Barista:
    class: Drupal\coffee_shop\Service\Barista
    arguments: ['@config.factory']

Then you are able to retrieve your service by the PHP class namespace from the service container (no additional development IDE plugin needed for service name suggestion):

$barista = \Drupal::getContainer()
  ->get(Barista::class);

Here is an example with the extra abstraction layer with a service machine name, which means too much work (old approach):

\Drupal::service('modulename.service_machinename');

Autowiring services

The autowiring functionality within the MODULENAME.services.yml file is a Symfony framework feature and not automatically tested by the Drupal core test suite. There is no Drupal core service which is autowired. Therefore, there is no consistent, tested workflow. Since Symfony 4 (new in Drupal 9) PHP classes are not autoresolved. If your services are autowired, but their injected classes are not, it is better to write your own service provider for your module than maintaining a MODULENAME.services.yml file in your Drupal module. See the following Drupal core issue, which addresses it: Document and add tests for service autowiring.

It's recommended to write your service provider PHP class, as next described.

Autowiring by the MODULENAME.services.yml file

Drupal can automatically wire services for you. The functionality differs between Drupal 8 and 9. This approach will potentially save you work within the *.services.yml file. After you have setup your service in the my_module.services.yml file, you can easily switch injected objects in your development workflow without modifying the .services.yml file.

The definition for an autowiring service is similar to the following one.

services:
  my_module.twitter_feed:
    class: Drupal\my_module\TwitterFeed
    autowire: true

If Drupal exceptionally cannot wire a service class for you, because any object cannot be found, then you can create an alias. The object could be not auto-registered because it is from a different root namespace and not defined as a service.

For example, the following service definition creates an alias for a dependency (Symfony\Component\DomCrawler\Crawler), which cannot be found in this example case.

  domcrawler.crawler:
    class: Symfony\Component\DomCrawler\Crawler
    public: false

The entire my_module.services.yml will look like the following.

services:
  my_module.twitter_feed:
    class: Drupal\my_module\TwitterFeed
    autowire: true

  domcrawler.crawler:
    class: Symfony\Component\DomCrawler\Crawler
    public: false

Service provider PHP class objects

In Drupal 9 some autowiring "magic" from Drupal 8 was removed, so PHP classes around your services are not autoresolved. Drupal also overrides the Symfony dependency injection by its own logic, therefore you must write your own service provider class in your module to autowire your service containers (basically all PHP classes, except e.g. Drupal Plugins or Drupal Entities, which Drupal 9 resolves by its own).

Service providers are supported since Drupal 8 and can be used in Drupal 9 in the same way.

Drupal automatically discovers only one service provider per Drupal module. The naming convention for the service provider PHP class is pascal cased Drupal module name plus ServiceProvider.php. You cannot name the service provider class just ServiceProvider.php. It must be [MODULE-NAME-PASCAL-CASE]ServiceProvider.php.

<?php
declare(strict_types=1);

namespace Drupal\my_nice_module;

use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\Finder\Finder;


class MyNiceModuleServiceProvider extends ServiceProviderBase {

  public function register(ContainerBuilder $container) {
    $containerModules = $container->getParameter('container.modules');
    $finder = new Finder();

    $foldersWithServiceContainers = [];

    $foldersWithServiceContainers['Drupal\my_nice_module\\'] = $finder->in(dirname($containerModules['my_nice_module']['pathname']) . '/src/')->files()->name('*.php');

    $foldersWithServiceContainers['Drupal\my_nice_module\Transformer\\'] = $finder->in(dirname($containerModules['my_nice_module']['pathname']) . '/src/Transformer/')->files()->name('*.php');

    $foldersWithServiceContainers['Drupal\my_nice_module\DomCrawler\\'] = $finder->in(dirname($containerModules['my_nice_module']['pathname']) . '/src/DomCrawler/')->files()->name('*.php');
    $foldersWithServiceContainers['Drupal\my_nice_module\MongoDBFetcher\\'] = $finder->in(dirname($containerModules['my_nice_module']['pathname']) . '/src/MongoDBFetcher/')->files()->name('*.php');

    $foldersWithServiceContainers['Drupal\my_nice_module\Importer\\'] = $finder->in(dirname($containerModules['my_nice_module']['pathname']) . '/src/Importer/')->files()->name('*.php');
    $foldersWithServiceContainers['Drupal\my_nice_module\Importer\MongoDB\\'] = $finder->in(dirname($containerModules['my_nice_module']['pathname']) . '/src/Importer/MongoDB/')->files()->name('*.php');

    foreach ($foldersWithServiceContainers as $namespace => $files) {
      foreach ($files as $fileInfo) {
        // remove .php extension from filename
        $class = $namespace
          . substr($fileInfo->getFilename(), 0, -4);
        // don't override any existing service
        if ($container->hasDefinition($class)) {
          continue;
        }
        $definition = new Definition($class);
        $definition->setAutowired(TRUE);
        $container->setDefinition($class, $definition);
      }
    }
  }

}

Afterwards, you are able to access your services via the service container.

$barista = \Drupal::getContainer()
  ->get(Barista::class);

The service name is the fully qualified name of namespace.

Exceptions

You could stumble into exceptions like the following one.

In DefinitionErrorExceptionPass.php line 54:
                                                                                                                                                                                                                                                                                                                       
  Cannot autowire service "importer.media_contact_entity_ids_for_node_determinator": argument "$entityTypeManager" of method "Drupal\importer\Determinator\MediaContactEntityIdsForNodeDeterminator::__construct()" references interface "Drupal\Core\Entity\EntityTypeManagerInterface"   
  but no such service exists. You should maybe alias this interface to the existing "entity_type.manager" service.                                                                                                                                                                                                     
                                                                                                                             

You can resolve them via aliasing in your MODULENAME.services.yml file.

Drupal\Core\Entity\EntityTypeManagerInterface:
  alias: entity_type.manager

Injecting dependencies into controllers, forms and blocks

Every object which uses Drupal\Core\DependencyInjection\ContainerInjectionInterface (for example, controllers, forms, and blocks), must implement the create() factory method to pass the dependencies into the class constructor. Controllers, forms, and blocks are not defined via the *.services.yml file. Check Dependency Injection for a Form.

Comparison to Drupal 7

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 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()->getCurrentLanguage() and Drupal::currentUser() respectively.

Further resources

Tags

Help improve this page

Page status: Needs review

You can: