Introduction

If you've spent time writing custom modules in Drupal, you've likely encountered service containers, service definitions, and the general idea of "injecting" dependencies into your classes. But for many Drupal developers, Dependency Injection (DI) remains something they use mechanically without truly understanding the architectural reasoning behind it.

This article breaks down what Dependency Injection actually is, why Drupal uses it, and how to implement it properly in your custom modules — with a particular focus on real-world patterns you'll encounter in production Drupal codebases.

What Is Dependency Injection?

At its core, Dependency Injection is a design pattern where an object receives its dependencies from external sources rather than creating them internally.

Without DI (tightly coupled)

class ArticleManager {
  public function getLatestArticles() {
    // Directly creating a dependency — tight coupling
    $database = \Drupal::database();
    $entityTypeManager = \Drupal::entityTypeManager();
    
    return $entityTypeManager
      ->getStorage('node')
      ->loadByProperties(['type' => 'article', 'status' => 1]);
  }
}

With DI (loosely coupled)

class ArticleManager {
  public function __construct(
    protected EntityTypeManagerInterface $entityTypeManager,
    protected Connection $database,
  ) {}

  public function getLatestArticles() {
    return $this->entityTypeManager
      ->getStorage('node')
      ->loadByProperties(['type' => 'article', 'status' => 1]);
  }
}

Why Does This Matter?

1. Testability — The non-DI version is nearly impossible to unit test. You can't run \Drupal::database() without a full Drupal bootstrap. With DI, you can inject mock objects:

$mockEntityTypeManager = $this->createMock(EntityTypeManagerInterface::class);
$mockDatabase = $this->createMock(Connection::class);

$manager = new ArticleManager($mockEntityTypeManager, $mockDatabase);

2. Loose Coupling — Your class depends on interfaces, not implementations. If Drupal changes how EntityTypeManager works internally, your code doesn't break.

3. Single Responsibility — DI forces you to think about what your class actually needs. If a constructor has 8 dependencies, it's a code smell.

4. Configurability — Different environments can inject different implementations. Your production code might use MySQL, while tests use SQLite.

Drupal's Service Container

Drupal uses Symfony's DependencyInjection component as its service container. Services are defined in a module's *.services.yml file:

# mymodule.services.yml
services:
  mymodule.article_manager:
    class: Drupal\mymodule\ArticleManager
    arguments:
      - '@entity_type.manager'
      - '@database'

The @ prefix means "inject the service with this ID." Drupal resolves these at runtime and passes them to your constructor.

Implementing DI in Custom Modules

Step 1: Define your service class

namespace Drupal\mymodule;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Database\Connection;

class ArticleManager {
  public function __construct(
    protected EntityTypeManagerInterface $entityTypeManager,
    protected Connection $database,
  ) {}

  public function getPublishedArticles(int $limit = 10): array {
    $storage = $this->entityTypeManager->getStorage('node');
    $ids = $storage->getQuery()
      ->condition('type', 'article')
      ->condition('status', 1)
      ->sort('created', 'DESC')
      ->range(0, $limit)
      ->accessCheck(TRUE)
      ->execute();

    return $storage->loadMultiple($ids);
  }
}

Step 2: Register it as a service

# mymodule.services.yml
services:
  mymodule.article_manager:
    class: Drupal\mymodule\ArticleManager
    arguments:
      - '@entity_type.manager'
      - '@database'

Step 3: Inject it in a Controller

namespace Drupal\mymodule\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\mymodule\ArticleManager;
use Symfony\Component\DependencyInjection\ContainerInterface;

class ArticleController extends ControllerBase {
  public function __construct(
    protected ArticleManager $articleManager,
  ) {}

  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('mymodule.article_manager'),
    );
  }

  public function listArticles() {
    $articles = $this->articleManager->getPublishedArticles(20);
    // Build render array...
  }
}

DI in Different Drupal Contexts

Forms

class ArticleFilterForm extends FormBase {
  public function __construct(
    protected ArticleManager $articleManager,
  ) {}

  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('mymodule.article_manager'),
    );
  }

  public function getFormId() {
    return 'article_filter_form';
  }
}

Plugins (Blocks)

Plugins use ContainerFactoryPluginInterface:

class LatestArticlesBlock extends BlockBase
  implements ContainerFactoryPluginInterface {

  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    protected ArticleManager $articleManager,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
  }

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

  public function build() {
    $articles = $this->articleManager->getPublishedArticles(5);
    // Return render array...
  }
}

Common Anti-Patterns

❌ Using \Drupal::service() inside a service class — If you're already in a service, inject your dependencies through the constructor. The static \Drupal helper is meant for procedural code (.module files, hooks) where DI isn't available.

❌ Injecting the entire container — This is called the "Service Locator" anti-pattern. It hides your real dependencies and makes testing harder.

❌ Constructor bloat — If your constructor has more than 5-6 dependencies, your class is likely doing too much. Break it into smaller, focused services.

Advanced: Service Decoration

Drupal's DI container supports service decoration — replacing or wrapping existing services:

services:
  mymodule.custom_entity_type_manager:
    class: Drupal\mymodule\CustomEntityTypeManager
    decorates: entity_type.manager
    arguments:
      - '@mymodule.custom_entity_type_manager.inner'

Key Takeaways

  • Inject, don't create — Always receive dependencies through constructors
  • Depend on interfaces — Reference EntityTypeManagerInterface, not EntityTypeManager
  • Use services.yml — Register every custom service explicitly
  • Use create() for non-services — Controllers, Forms, and Plugins use factory methods
  • Keep constructors lean — More than 5-6 dependencies is a code smell
  • Avoid static helpers in services\Drupal::service() is for procedural code only

Conclusion

Dependency Injection in Drupal isn't just a technical formality — it's the foundation of clean, testable, and maintainable architecture. Understanding DI transforms you from someone who writes Drupal modules to someone who engineers them.