Skip to content

Essential APIs - Services and Dependency Injection

From Services and dependency injection in Drupal on Drupal.org updated July 2024

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.

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.

The Services and Dependency Injection Container concepts have been adopted by Drupal from the Symfony DependencyInjection component. A service (such as accessing the database, sending email, or translating user interface text) is defined (given a name and an interface or at least a class that defines the methods that may be called), and a default class is designated to provide the service. These two steps must be done together, and can be done by Drupal Core or a module. Other modules can then define alternative classes to provide the same services, overriding the default classes. Classes and functions that need to use the service should always instantiate the class via the dependency injection container (also known simply as the container), rather than instantiating a particular service provider class directly, so that they get the correct class (default or overridden).

See Services and dependency injection in Drupal on Drupal.org updated July 2024 for more detailed information on services and the dependency injection container.

Services can depend on other services.

For example path_alias.manager depends on path_alias_storage, path_alias_whitelist, language_manager, cache.data and datetime.time:

from web/core/modules/path_alias/path_alias.services.yml:

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

This ensures these services are passed into the path_alias.manager constructor:

php
<?php
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.
   * @param \Drupal\Component\Datetime\TimeInterface|null $time
   *   The time service.
   */
  public function __construct(
    AliasRepositoryInterface $alias_repository,
    AliasWhitelistInterface $whitelist,
    LanguageManagerInterface $language_manager,
    CacheBackendInterface $cache,
    protected ?TimeInterface $time = NULL,
  ) {
    $this->pathAliasRepository = $alias_repository;
    $this->languageManager = $language_manager;
    $this->whitelist = $whitelist;
    $this->cache = $cache;
    if (!$time) {
      @trigger_error('Calling ' . __METHOD__ . '() without the $time argument is deprecated in drupal:10.3.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/3387233', E_USER_DEPRECATED);
      $this->time = \Drupal::service(TimeInterface::class);
    }
  }

  // ...
}
?>

Accessing Service Through the Container

When dependency injection is not applicable, you can use a service container to implement a service.

Plugin classes, controllers, and similar classes have create() or createInstance() methods that are used to create an instance of the class. These methods come from different interfaces, and have different arguments, but they all include an argument $container of type \Symfony\Component\DependencyInjection\ContainerInterface. If you are defining one of these classes, in the create() or createInstance() method, call $container->get('myservice.name') to instantiate a service. The results of these calls are generally passed to the class constructor and saved as member variables in the class.

More at Services and Dependency Injection Container on Drupal.org:

Access Services using helper methods

If neither of the previous methods apply, Drupal can look up the service for you by using \Drupal::service('service.name').

Drupal also provides many special methods for accessing common services. For example:

  • \Drupal::cache($bin) - Retrieves a particular cache bin based on specified parameter.
  • \Drupal::config($name) - Retrieves a configuration object based on specified parameter.
  • \Drupal::currentUser() - Retrieves current user service.
  • \Drupal::database() - Retrieves database service.
  • \Drupal::entityTypeManager() - Retrieves the entity type manager service which is helpful for interacting with entities.

See class Drupal for more helper methods.

Service Tags

From Service Tags on Drupal.org:

Some services have tags, which are defined in the service definition. Tags are used to define a group of related services, or to specify some aspect of how the service behaves. Typically, if you tag a service, your service class must also implement a corresponding interface. Some common examples:

  • access_check: Indicates a route access checking service; see the Menu and routing system topic for more information.
  • cache.bin: Indicates a cache bin service; see the Cache topic for more information.
  • event_subscriber: Indicates an event subscriber service. Event subscribers can be used for dynamic routing and route altering; see the Menu and routing system topic for more information. They can also be used for other purposes; see http://symfony.com/doc/current/cookbook/doctrine/event_listeners_subscribers.html for more information.
  • needs_destruction: Indicates that a destruct() method needs to be called at the end of a request to finalize operations, if this service was instantiated. Services should implement \Drupal\Core\DestructableInterface in this case.
  • context_provider: Indicates a block context provider, used for example by block conditions. It has to implement \Drupal\Core\Plugin\Context\ContextProviderInterface.
  • http_client_middleware: Indicates that the service provides a guzzle middleware, see https://guzzle.readthedocs.org/en/latest/handlers-and-middleware.html for more information.

Creating a tag for a service does not do anything on its own, but tags can be discovered or queried in a compiler pass when the container is built, and a corresponding action can be taken. See web/core/lib/Drupal/Core/Render/MainContent/MainContentRenderersPass.php for an example of finding tagged services:

php
/**
 * Adds main_content_renderers parameter to the container.
 */
class MainContentRenderersPass implements CompilerPassInterface {

  /**
   * {@inheritdoc}
   *
   * Collects the available main content renderer service IDs into the
   * main_content_renderers parameter, keyed by format.
   */
  public function process(ContainerBuilder $container) {
    $main_content_renderers = [];
    foreach ($container->findTaggedServiceIds('render.main_content_renderer') as $id => $attributes_list) {
      foreach ($attributes_list as $attributes) {
        $format = $attributes['format'];
        $main_content_renderers[$format] = $id;
      }
    }
    $container->setParameter('main_content_renderers', $main_content_renderers);
  }

}

Overriding Services

To override the default classes used for existing services do the following:

  • Create a [Module]ServiceProvider class in the top-level of your module, but swap out [Module] for the camel-cased name of your module. For example, the inline_form_errors module uses InlineFormErrorsServiceProvider.
  • Service provider needs to implement \Drupal\Core\DependencyInjection\ServiceModifierInterface (which is commonly done by extending \Drupal\Core\DependencyInjection\ServiceProviderBase).
  • Add an alter() method to require Drupal use your class instead.

Example from web/core/modules/inline_form_errors/src/InlineFormErrorsServiceProvider.php:

php
<?php

namespace Drupal\inline_form_errors;

use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Overrides the form_error_handler service to enable inline form errors.
 */
class InlineFormErrorsServiceProvider extends ServiceProviderBase {

  /**
   * {@inheritdoc}
   */
  public function alter(ContainerBuilder $container) {
    $container->getDefinition('form_error_handler')
      ->setClass(FormErrorHandler::class)
      ->setArguments([
        new Reference('string_translation'),
        new Reference('renderer'),
        new Reference('messenger'),
      ]);
  }

}

Decorating Services

From symfony.com - How to Decorate Services:

When overriding an existing definition (e.g. when applying the Decorator pattern), the original service is lost:

yaml
# config/services.yaml
services:
  App\Mailer: ~

  # this replaces the old App\Mailer definition with the new one, the
  # old definition is lost
  App\Mailer:
    class: App\NewMailer

Most of the time, that's exactly what you want to do. But sometimes, you might want to decorate the old one instead (i.e. apply the Decorator pattern). In this case, the old service should be kept around to be able to reference it in the new one. This configuration replaces App\Mailer with a new one, but keeps a reference of the old one as .inner:

yaml
# config/services.yaml
services:
  App\Mailer: ~

  App\DecoratingMailer:
    # overrides the App\Mailer service
    # but that service is still available as ".inner"
    decorates: App\Mailer

The decorates option tells the container that the App\DecoratingMailer service replaces the App\Mailer service. If you're using the default services.yaml configuration, the decorated service is automatically injected when the constructor of the decorating service has one argument type-hinted with the decorated service class.

If you are not using autowiring or the decorating service has more than one constructor argument type-hinted with the decorated service class, you must inject the decorated service explicitly (the ID of the decorated service is automatically changed to .inner):

yaml
# config/services.yaml
services:
    App\Mailer: ~

    App\DecoratingMailer:
        decorates: App\Mailer
        # pass the old service as an argument
        arguments: ['@.inner']

The visibility of the decorated App\Mailer service (which is an alias for the new service) will still be the same as the original App\Mailer visibility.

yaml
# config/services.yaml
services:
    App\DecoratingMailer:
        # ...
        decoration_inner_name: App\DecoratingMailer.wooz
        arguments: ['@App\DecoratingMailer.wooz']

Decoration Priority

When applying multiple decorators to a service, you can control their order with the decoration_priority option. Its value is an integer that defaults to 0 and higher priorities mean that decorators will be applied earlier.

yaml
# config/services.yaml
services:
    Foo: ~

    Bar:
        decorates: Foo
        decoration_priority: 5
        arguments: ['@.inner']

    Baz:
        decorates: Foo
        decoration_priority: 1
        arguments: ['@.inner']

The generated code will be the following:

php
$this->services[Foo::class] = new Baz(new Bar(new Foo()));

More at How to Decorate Services

Questions

How to use Entity Type Manager service to load an entity by condition field_test = "test" and content type is "article"?
php
\Drupal::entityTypeManager()->loadByProperties([
    'field_test' => 'test',
    'type' => 'article',
]);

Note: loadByProperties bypasses access checks EntityStorageBase::loadByProperties. If you need access checks, use getQuery() instead.

How can you alter an existing service in Drupal?

To alter an existing service in Drupal, you can use either service subscribers or the decorator pattern:

  1. Using a Service Subscriber:

    • Create a [ModuleName]ServiceProvider class extending ServiceProviderBase.
    • Implement the alter() method to modify the service using $container->getDefinition().
    • Update the service class, arguments, or behavior.
    • Register the provider in your module’s .info.yml file.
  2. Using the Decorator Pattern:

    • Define a new service in your services.yml file with the decorates key, specifying the service to override.
    • The original service is passed to the new one as an .inner argument.
    • Implement the decorator class to wrap or extend the functionality of the original service.
    • Use decoration_priority to control the order of decorators if multiple exist.

Resources