
В современной разработке на 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 и используется для:
- Регистрации и определения сервисов.
- Создания их экземпляров по необходимости.
- Управления их зависимостями.
- Эффективного повторного использования во всем приложении.
Пример определения базового сервиса в кастомном модуле:
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() может казаться безобидным. Но в крупных сайтах и корпоративных приложениях разница становится критической:
- Тестируемость. Сервисы, внедренные через DI, легко заменяются «заглушками» (mocks) в юнит-тестах. Статические вызовы заменить сложно, что усложняет автоматизацию тестирования.
- Поддерживаемость. Если изменится зависимость, вам нужно изменить её только в одном месте — в определении сервиса. Вам не придется искать и менять код во множестве файлов.
- Разделение ответственности (Separation of concerns). Бизнес-логика остается чистой и отделенной от внутренних механизмов Drupal. Каждый сервис отвечает за одну конкретную задачу.
- Готовность к 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. Эти паттерны — уже не «продвинутые концепции», это норма построения качественных систем. Чем раньше вы с ними подружитесь, тем легче будет ваша будущая разработка.