Dependency injection

Published at

March 10, 2020

Author

Erlend ter Maat

Tags

Drupal, Drupal service container, Dependency injection


Drupal services offer a way to expose functionality to other modules. Other modules commonly use that services by means of dependency injection.

Technical implementation

Dependency injection requires a mapping where the services are defined, and a way to inject the services into objects as they are loaded. Drupal borrowed the dependency injection functionality from the symfony framework. Hence, Services are stored in yaml files. A basic example.

services:
  custom_service:
    class: [ class name ]

  another_service:
    class: [ another class name ]

Frameworks exists, like Masonite (Python) or Laravel (PHP), where services are recognized automatically. At drupal you have to be explicit. Next two examples show the two common ways to add a service by the id of ‘another_service’ to the ‘custom_service’:

By means of services

At the services.yml file the dependency is defined a an argument.

# module-name.services.yml
services:
  custom_service:
    class: [ class name ]
    arguments:
      - '@another_service'

At the implementation of the class the dependencies are implemented / processed at the constructor.

class CustomService {

  protected $anotherService;

  public function __construct(AnotherServiceInterface $anotherService) {
    $this->anotherService = $anotherService;
  }

}

By means of Service Containers

Providing a create method that knows what other services are required to build the service:

class CustomServiceContainingClass implements ContainerInjectionInterface {

  public static function create(Container $container) {
    // Create an instance of this class using services from the service container.
    return new static($container->get('another_service');
  }

  protected $anotherService;

  public function __construct(AnotherServiceInterface $anotherService) {
    $this->anotherService = $anotherService;
  }

}

A ContainerInjection enabled class may be defined as a service, but services don’t require the ContainerInjectionInterface. You only need to implement the ContainerInjectionInterface if your service is not always loaded by the drupal services system; but only/also by the class resolver.

Class resolver service

The most common case of dependency injection is by means of services - classes that require one instance per request. Dependency injection can also be used for classes where multiple instances of the same class are required / make more sense.

services:
  custom_service:
    class: CustomService
    arguments:
      - '@class_resolver'
class CustomService {

  function usingTheClassResolver() {
    $containingInstance = $this
                            ->classResolver
                            ->getInstanceFromDefinition(
                              CustomServiceContainingClass::class
                            );
  }

}

Benefits

The clean thing about using this is that when something changes in the dependencies of the CustomServiceContainingClass that is the only place where the change of code happens. Without a dependency injection a developer should change all classes that lead to the place where de dependency is used. Now you add a class resolver as dependency, and this object manages the dependencies at the place where they are needed. The combination of services and dependency injection offers a nice way to implement the single responsibility principle.

Dependency injection is a mechanism to inventorize the external dependencies of a class. It allows you to move code that solves another problem out of the scope of code that solves a specific business case.