Архитектура Drupal: как связаны контейнер, сервисы и плагины
В Drupal контейнер отвечает за зависимости, сервисы — за повторно используемую логику, а плагины — за расширяемые точки системы. Разбираем границы между ними и типичные ошибки при проектировании модулей.

В Drupal 10.3+ и 11.x одна и та же задача часто проходит через три разных механизма: контейнер собирает зависимости, сервис выполняет бизнес-логику, а плагин подключает реализацию к расширяемой точке ядра. Если смешать эти роли, модуль начинает зависеть от глобального состояния, плагины трудно тестировать, а замена одной реализации превращается в переписывание нескольких классов.
Три слоя, которые нельзя путать
Контейнер, сервисы и плагины связаны, но не являются синонимами. Контейнер не хранит бизнес-логику, сервис не обязан быть плагином, а плагин не должен самостоятельно искать зависимости через глобальный вызов.
| Механизм | За что отвечает | Как обычно получают | Типичная ошибка |
|---|---|---|---|
| Dependency injection container | Создаёт объекты и передаёт им зависимости | Через конструктор или фабрику плагина | Получать сервисы напрямую внутри метода |
| Сервис | Повторно используемая операция или интеграция | Через аргумент конструктора | Складывать в один класс контроллер, запросы и бизнес-правила |
| Плагин | Реализация расширяемой точки Drupal | Через plugin manager | Считать любой класс модуля плагином |
Контейнер зависимостей — объект, который знает, как собрать зарегистрированные сервисы и какие аргументы передать их конструкторам. В Drupal он построен на компонентах Symfony, но правила регистрации и точки интеграции задаёт Drupal.
Плагин — класс с метаданными, который Drupal может найти, выбрать по идентификатору и создать через соответствующий менеджер плагинов.[1] Например, блоки, обработчики полей, форматтеры, источники миграции и очереди используют plugin API, но каждый тип имеет собственный менеджер и контракт.
Правильный вопрос звучит не «куда положить этот класс», а «кто управляет его жизненным циклом и по какому контракту его будут находить».
Как проходит вызов внутри модуля
Представим модуль, который показывает цену товара с учётом внешнего курса валют. Схема должна выглядеть так:
- Плагин или контроллер принимает входные данные Drupal: конфигурацию блока, сущность или параметры маршрута.
- Сервис модуля применяет правило расчёта и обращается к шлюзу курса.
- Другой сервис выполняет HTTP-запрос, кеширование или журналирование.
- Контейнер создаёт эти объекты и передаёт их через конструкторы.
Плагин при этом не должен знать, как устроен HTTP-клиент. Ему достаточно получить сервис с операцией вроде «рассчитать цену». Такая граница позволяет заменить источник курса, не меняя сам плагин.
Сервис: где держать бизнес-логику
Сервис подходит для кода, который нужен нескольким точкам входа: контроллеру, обработчику формы, плагину блока, Drush-команде или очереди. Регистрация выполняется в файле my_module.services.yml.
services:
my_module.price_calculator:
class: Drupal\my_module\Service\PriceCalculator
arguments:
- '@logger.channel.my_module'
logger.channel.my_module:
parent: logger.channel_base
arguments:
- 'my_module'
Класс получает зависимость в конструкторе, а не извлекает её из контейнера во время работы:
namespace Drupal\my_module\Service;
use Psr\Log\LoggerInterface;
final class PriceCalculator {
public function __construct(
private readonly LoggerInterface $logger,
) {}
public function calculate(float $basePrice, float $exchangeRate): float {
if ($basePrice < 0 || $exchangeRate <= 0) {
throw new \InvalidArgumentException('Price and rate must be positive.');
}
$price = round($basePrice * $exchangeRate, 2);
$this->logger->info('Price calculated: {price}', ['price' => $price]);
return $price;
}
}
После изменения *.services.yml Drupal обычно нужно перестроить кеш контейнера. В рабочей среде это делают штатной командой управления сайтом, например через Drush, с учётом способа деплоя проекта. Сам факт наличия файла не означает, что уже запущенный процесс увидел новую регистрацию.
Как внедрять сервис в плагин
Плагин создаёт не ваш код, а plugin manager. Поэтому обычный конструктор с дополнительным аргументом сам по себе не сработает: менеджеру нужно объяснить, как передать контейнерную зависимость.
Для плагинов, поддерживающих контейнерную фабрику, используется ContainerFactoryPluginInterface. Фрагмент ниже показывает схему для Drupal 10.3+ и 11.x:
namespace Drupal\my_module\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\my_module\Service\PriceCalculator;
use Symfony\Component\DependencyInjection\ContainerInterface;
final class ProductPriceBlock extends BlockBase implements ContainerFactoryPluginInterface {
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
private readonly PriceCalculator $priceCalculator,
) {
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('my_module.price_calculator'),
);
}
public function build() {
$price = $this->priceCalculator->calculate(100, 1.1);
return ['#markup' => $this->t('Price: @price', ['@price' => $price])];
}
}
В реальном блоке цена и курс не должны быть захардкожены: их нужно получить из конфигурации, сущности или отдельного источника данных. Здесь числа нужны только для демонстрации границы между плагином и сервисом.
Плагин-менеджер и контейнер — разные уровни
Менеджер плагинов отвечает за поиск определений, фильтрацию по поддерживаемому типу и создание экземпляров. Он сам обычно зарегистрирован как сервис. Поэтому цепочка может выглядеть так: контейнер создаёт менеджер блоков, менеджер находит плагин блока, а фабрика плагина получает из контейнера сервис расчёта.
Это объясняет важное ограничение: нельзя автоматически считать любой плагин обычным контейнерным сервисом. Плагин имеет конфигурацию, идентификатор и определение, а его жизненный цикл контролирует менеджер конкретного типа.
Два способа получить зависимость
| Подход | Когда применять | Цена |
|---|---|---|
| Внедрение через конструктор | Сервисы, контроллеры, плагины с фабрикой | Нужно описать регистрацию и обновить контейнер после изменений |
| Получение из контейнера внутри метода | Редкие инфраструктурные переходники, где API требует контейнер | Зависимость скрыта, тестирование сложнее, ошибки проявляются позже |
Для обычного сервиса выбирайте конструктор. Вызов вроде \Drupal::service('my_module.price_calculator') внутри бизнес-метода делает зависимость невидимой. Это не всегда немедленно ломает сайт, но ухудшает тесты и связывает код с глобальным состоянием Drupal.
Где архитектура ломается
Плагин превращается в монолит
Если в build() одновременно читать сущность, формировать SQL-запрос, обращаться к внешнему API и собирать render array, класс трудно переиспользовать. Оставьте плагину адаптацию к контракту Drupal, а вычисления вынесите в сервисы.
Сервис знает о конкретном плагине
Обратная связь допустима только на уровне общего контракта. Если сервис импортирует класс конкретного блока или вызывает его методы, слои перепутаны. Сервис должен работать с данными и интерфейсами, а не с тем, кто его вызвал.
Кеш контейнера скрывает ошибку
После добавления или переименования сервиса сайт может продолжить использовать старое определение до перестроения кеша. При сообщении о неизвестном сервисе сначала проверьте имя, файл YAML, отступы и процесс обновления контейнера. Не удаляйте кеши наугад в продакшене: порядок операций зависит от схемы деплоя и числа веб-узлов.
Внешний запрос выполняется на каждом рендере
Плагин блока может участвовать в рендеринге множества страниц. Сетевой вызов внутри него без кеширования увеличивает задержку и создаёт зависимость от доступности внешней системы. Кеширование, тайм-ауты и обработка ошибок — отдельная часть дизайна интеграции, а не обязанность контейнера.
Как проверять такую структуру на ревью
- У каждого сервиса есть одна понятная ответственность и явные зависимости в конструкторе.
- Плагин использует контракт своего типа и не содержит код, который нужен другим точкам входа.
- Регистрация в
*.services.ymlсовпадает с namespace и именем класса. - Внешние вызовы имеют тайм-аут, понятную стратегию ошибок и предусмотренное кеширование.
- Тест сервиса может подставить тестовую зависимость без загрузки всего Drupal-сайта.
Для тестов полезнее начать с сервиса: его входы и выходы обычно проще зафиксировать. Плагин затем проверяют отдельно — прежде всего создание через фабрику и соответствие контракту Drupal.
Что делать дальше
Возьмите один существующий модуль и нарисуйте цепочку: точка входа, плагин или контроллер, сервисы, внешние зависимости. Всё, что сейчас вызывается через глобальный \Drupal или собирается прямо в плагине, пометьте кандидатом на внедрение через конструктор. После рефакторинга измеряйте не только тестовое покрытие, но и время рендера, число внешних запросов и ошибки контейнера после деплоя.
[1] Plugin manager — менеджер, который ищет определения плагинов одного типа и создаёт их экземпляры по идентификатору и конфигурации.
Архитектор и дизайнер интерьера: Законные перепланировки (БТИ), расчёт плитки, обоев, материалов.
Спросить за 15 ₽Источники: Drupal.org: Dependency Injection, Drupal API: Plugin system
Частые вопросы
Нужно ли регистрировать каждый класс Drupal-модуля как сервис?
Можно ли вызвать сервис через \Drupal::service()?
Почему Drupal не создаёт плагин с моим конструктором автоматически?
Что делать, если появился неизвестный сервис после изменения YAML?
Подходит ли эта схема для Drupal 9 или старых версий Drupal 10?
Нужно ли выносить в сервис каждый метод плагина?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.