Dependency Injection in Drupal

Ala Batayneh

Dependency Injection (DI) is a fundamental design pattern in object-oriented programming. Instead of having a class create its own dependencies, DI supplies them from the outside. This approach keeps classes loosely coupled and easier to maintain and test.

By separating classes from the responsibility of creating their dependencies, we promotes flexibility, scalability, and cleaner architecture.

In this article, we’ll explore the concept of Dependency Injection, walk through its implementation, highlight its benefits, and share real-world examples, applications, and best practices.

 

What is Dependency Injection?

Dependency Injection is an application of the Inversion of Control Principle, so rather than instantiating dependencies within the class, they are injected through constructors, setters, or interfaces. Dependency Injection guiding rule states that high-level modules should not depend on low-level modules, but both should depend on abstractions.

In Drupal, Dependency Injection is made possible through service containers; a central registry that manages services (e.g., sending emails or caching data). These services are defined in YAML files, and can be injected into classes such as controllers, forms, blocks, or plugins. 

Before Drupal 8, developers often relied on procedural code or static methods, which led to tightly-coupled hard-to-test systems. Dependency Injection allows implementations to be swapped, and makes unit testing much easier.

 

 

Service Containers in Drupal

Service containers are the heart of Drupal's DI system, often referred to as the Dependency Injection Container (DIC), it is responsible for creating services and wiring them together.

When Drupal bootstraps, it compiles all the containers from service definitions provided by Drupal and the contributed modules.

Services in Drupal are defined in a module's *.service.yaml file. For example, a basic service definition would look like:

services:
  my_module.example_service:
    class: Drupal\my_module\ExampleService
    arguments: ['@database', '@logger.factory']

Here, my_module.example_service is the service ID. The class specifies the PHP class to instantiate, and the arguments list the dependencies to inject (prefixed with @ for other services). This structure allows Drupal to resolve injection dependencies recursively. And to optimize performance, containers support lazy loading, where services are only instantiated when first requested.

It's possible to use \Drupal::getContainer() to access the container programmatically, but this is generally considered a bad practice.

 

Implementing Dependency Injection in Drupal Components

One common use case is injecting services into forms. Forms often need access to entities or configurations (and sometimes other services). So instead of using static class like \Drupal::entityTypeManager(), services should be injected instead.

For a custom form class extending FormBase:

namespace Drupal\my_module\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class MyForm extends FormBase {

  protected $entityTypeManager;

  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
    $this->entityTypeManager = $entity_type_manager;
  }

  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('entity_type.manager')
    );
  }

  // Form methods here...
}

Here, EntityTypeManagerInterface is injected via the constructor, and the create() method fetches it from the container. This patterns allows instantiating forms for testing without a full Drupal environment.

Similarly, for blocks (or plugins), DI is applied in the plugin class. Blocks might need services like current user for example.

namespace Drupal\my_module\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class MyBlock extends BlockBase {

  protected $currentUser;

  public function __construct(array $configuration, $plugin_id, $plugin_definition, AccountInterface $current_user) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->currentUser = $current_user;
  }

  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('current_user')
    );
  }

  // build method here...
}

 

This kind of injection allows the block to access the current user without static calls.

Additionally, controllers also benefit from DI. In routing YAML file, controller class can be specified with the injected dependencies. For instance:

my_module.route:
  path: '/my-route'
  defaults:
    _controller: 'Drupal\my_module\Controller\MyController::content'

 

Benefits of Dependency Injection in Drupal

Adopting DI patterns enhanced modularity by swapping or extending services without altering dependent classes. It also improves testability by allowing injecting mock data, so unit tests can be written in full isolation. It also enhances performance and encourages better code organization. Furthermore, the use of lazy services minimizes overhead.

Dependency Injection enhances maintainability and reduces spaghetti code, which eventually makes the whole application less prone to errors and easier to debug.

     

    Tags, Decorators, and Altering Services

    For more advanced scenarios, Drupal's DI support service tags. Tags categorize services for collection by other services. For example, the event_subscriber tag registers classes as event listeners automatically:

    services:
      my_module.subscriber:
        class: Drupal\my_module\EventSubscriber\MySubscriber
        tags:
          - { name: event_subscriber }

    Decorators allow wrapping existing services to modify behavior without changing the original, this is particularly useful for extending core services.

    Modules can implement hook_service_alter() or use service provider classes to override definitions, this is called Altering Services.

    For dynamic services, factories can create instances based on runtime conditions - pretty powerful stuff.

    In plugins, DI is integrated via the plugin manager which often injects dependencies into plugin instances.

     

    Best Practices

    • Use injections over static calls: Avoid using  \Drupal::service() or \Drupal::currentUser() in classes.
    • Use meaningful services IDs and specify interfaces in arguments.
    • Inject interfaces rather than concrete classes to allow substitutions.
    • For config-dependent services, inject config.factory and load configs dynamically.
    • Don't inject the whole containers, inject only what is needed to avoid overhead.
    • For classes not under full container control, use factory methods and implement create() as mentioned earlier.
    • Always clear the cache after updating service files.

     

    Dependency injection is a modern powerful patterns that transforms development process, fostering clean, extensible code, whether you're building forms, blocks, or custom services, Dependency Injection will elevate codebase overall quality.