Что такое Service Container и Dependency Injection в Drupal

alexei13/08/2026 - 08:30
Что такое Service Container и Dependency Injection в Drupal

В современной разработке на Drupal использование контейнера сервисов и внедрения зависимостей (DI) заменило глобальные статические вызовы вроде \Drupal::currentUser() в качестве стандарта доступа к переиспользуемому функционалу. Простыми словами: это более чистый способ организации кода, который делает его легким для тестирования, исправления и повторного использования.

Service Container (контейнер сервисов) — это центральный реестр PHP-объектов (сервисов) в Drupal. Он управляет ими, создает экземпляры и переиспользует их, чтобы вам не приходилось создавать их вручную. Dependency Injection (внедрение зависимостей) — это паттерн, который передает эти объекты в класс через его конструктор, вместо того чтобы класс сам «вытягивал» их из глобального локатора сервисов. Если вы создаете кастомные модули, контроллеры, плагины или подписчиков событий (event subscribers), понимание обоих понятий — это не опция, а необходимость.

В старых модулях вы всё еще можете встретить такой код:


$user = \Drupal::currentUser();

Такой подход работает, но переход Drupal на архитектуру на базе Symfony сделал его исключением, а не стандартом.

Ключевые выводы

  • Контейнер сервисов централизованно регистрирует и управляет переиспользуемыми PHP-объектами, избавляя от необходимости создавать их вручную.
  • Внедрение зависимостей (DI) передает зависимости класса через его конструктор (вместо использования \Drupal::service()), что делает код тестируемым и поддерживаемым.
  • Кастомные сервисы определяются в файле *.services.yml модуля, создаются как классы и вызываются в контроллерах через метод create().
  • Druplus 11 добавляет поддержку PHP 8+ (Constructor Property Promotion), что позволяет писать более лаконичные классы сервисов.
  • Чего стоит избегать: вызовов \Drupal::service() внутри классов, перегрузки класса слишком большим количеством внедренных сервисов и типизации конкретных классов вместо интерфейсов.

Что такое сервис (Service) в Drupal?

Сервис в Drupal — это просто PHP-объект, который выполняет определенные задачи. Вместо того чтобы создавать экземпляры объектов по всему коду, Drupal позволяет зарегистрировать их в одном месте для легкого повторного использования.

Примеры распространенных сервисов в ядре (core) Drupal:

  • entity_type.manager — загружает и управляет сущностями (entities).
  • logger.factory — логирует активность приложения.
  • config.factory — читает конфигурации приложения.
  • path.current — получает текущий путь страницы.

Все эти сервисы регистрируются в файлах .services.yml и хранятся в контейнере сервисов Drupal.

Что такое Service Container?

Контейнер сервисов (или DI Container) — это центральный реестр всех сервисов в Drupal. Он основан на компоненте Dependency Injection от Symfony и используется для:

  1. Регистрации и определения сервисов.
  2. Создания их экземпляров по необходимости.
  3. Управления их зависимостями.
  4. Эффективного повторного использования во всем приложении.

Пример определения базового сервиса в кастомном модуле:


services:
  my_module.custom_service:
    class: Drupal\my_module\CustomService
    arguments: ['@current_user', '@logger.factory']

Символ @ указывает, что это другие сервисы, которые уже зарегистрированы. Контейнер берет на себя всю работу по их сборке.

Что такое Dependency Injection (DI)?

DI — это паттерн проектирования, при котором объект получает свои зависимости извне, а не создает их внутри себя.

Разница между Service Locator и DI: в одном паттерне класс сам «лезет» в контейнер за сервисом, в другом — контейнер «отдает» сервис классу.

БЕЗ использования Dependency Injection (плохо):


class CustomClass {
  public function loadNode($nid) {
    $entity_type_manager = \Drupal::service('entity_type.manager');
    return $entity_type_manager->getStorage('node')->load($nid);
  }
}

Этот код будет работать, но он жестко связывает ваш класс с глобальным локатором сервисов Drupal. Это затрудняет тестирование и поддержку, а также нарушает стандарты кодирования Drupal.

С использованием Dependency Injection (хорошо):


use Drupal\Core\Entity\EntityTypeManagerInterface;

class CustomClass {
  protected $entityTypeManager;

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

  public function loadNode($nid) {
    return $this->entityTypeManager->getStorage('node')->load($nid);
  }
}

Здесь зависимость внедряется в класс через конструктор. Классу неважно, как создан EntityTypeManager. Он просто его использует. Это делает код чистым и тестируемым.


Как создать базовый кастомный сервис в Drupal?

Шаг 1: Определите сервис (в файле my_module.services.yml)


services:
  my_module.custom_service:
    class: Drupal\my_module\CustomService
    arguments: ['@current_user', '@logger.factory']

Шаг 2: Добавьте класс сервиса (src/CustomService.php)


namespace Drupal\my_module;

use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;

class CustomService {
  protected $currentUser;
  protected $logger;

  public function __construct(AccountProxyInterface $current_user, LoggerChannelFactoryInterface $logger_factory) {
    $this->currentUser = $current_user;
    $this->logger = $logger_factory->get('my_module');
  }

  public function customFunc() {
    $name = $this->currentUser->getDisplayName();
    $this->logger->info('Logger info @name', ['@name' => $name]);
    return "Hello, $name!";
  }
}

Шаг 3: Используйте его в контроллере


use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\my_module\CustomService;

class GreetingController extends ControllerBase {
  protected $customService;

  public function __construct(CustomService $custom_service) {
    $this->customService = $custom_service;
  }

  // Метод create() — это способ Drupal извлекать сервисы из контейнера 
  // и передавать их в конструктор. Это стандарт для контроллеров и форм.
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('my_module.custom_service')
    );
  }

  public function content() {
    return [
      '#markup' => $this->customService->customFunc(),
    ];
  }
}


Почему DI важен для проектов?

В маленьких и быстрых сборках вызов \Drupal::service() может казаться безобидным. Но в крупных сайтах и корпоративных приложениях разница становится критической:

  1. Тестируемость. Сервисы, внедренные через DI, легко заменяются «заглушками» (mocks) в юнит-тестах. Статические вызовы заменить сложно, что усложняет автоматизацию тестирования.
  2. Поддерживаемость. Если изменится зависимость, вам нужно изменить её только в одном месте — в определении сервиса. Вам не придется искать и менять код во множестве файлов.
  3. Разделение ответственности (Separation of concerns). Бизнес-логика остается чистой и отделенной от внутренних механизмов Drupal. Каждый сервис отвечает за одну конкретную задачу.
  4. Готовность к Drupal 11. В Drupal 11 удалено множество устаревших (deprecated) паттернов статического использования сервисов. Использование DI сегодня гарантирует меньше проблем при переходе на следующую мажорную версию.

Что изменится в Drupal 11 для сервисов и DI?

Drupal 11 работает на Symfony 7 и требует PHP 8.2+. Это влияет на три аспекта:

  • Constructor Property Promotion: Теперь можно объявлять и инициализировать свойства прямо в аргументах конструктора (меньше кода):
    
    public function __construct(
      protected AccountProxyInterface $currentUser,
      protected LoggerChannelFactoryInterface $loggerFactory,
    ) {}
    
    
  • Read-only свойства: Где зависимость сервиса не меняется после создания, можно пометить её как readonly. Это защищает от случайного переназначения свойства внутри класса.
  • Сближение с Symfony: Drupal 11 всё ближе к стандартам Symfony 7. Использование современных конвенций позволит вам легче обновляться в будущем.

Распространенные ошибки: чего стоит избегать?

  • Стоит ли использовать \Drupal::service() внутри класса?
    Нет. Оставьте \Drupal::service() для процедурного кода (например, реализации хуков). Вызов внутри класса привязывает его к глобальному локатору, что мешает тестированию.
  • Сколько сервисов можно внедрять в один класс?
    Как можно меньше, сколько реально необходимо. Если в конструкторе 5-6 сервисов — это признак того, что класс делает слишком много. Разбейте его на более мелкие, сфокусированные сервисы.
  • Нужно ли указывать интерфейс или конкретный класс?
    Всегда используйте интерфейсы (например, EntityTypeManagerInterface вместо EntityTypeManager). Интерфейсы позволяют легко заменить реализацию (например, в тестах), не ломая весь остальной код.

Заключительные мысли

Использование контейнера сервисов и DI может казаться избыточной работой на первых порах. Но как только вы освоите их, ваш код станет чище и проще в управлении. Отладка упростится, а проект будет выглядеть организованным, а не хаотичным.

Drupal движется в сторону Symfony и современных практик PHP. Эти паттерны — уже не «продвинутые концепции», это норма построения качественных систем. Чем раньше вы с ними подружитесь, тем легче будет ваша будущая разработка.