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

Если модулю Drupal нужно вывести страницу, выполнить реакцию на событие или дать несколько взаимозаменяемых реализаций, у вас есть разные точки расширения. Хук быстрее написать, контроллер нужен для маршрута, сервис отделяет логику, а плагин оправдан только там, где действительно предусмотрены варианты. Ниже — сравнение для Drupal 10.3–11.x и практическое правило выбора, чтобы не строить сложную архитектуру вокруг одной функции.
Сначала определите границу модуля
Перед созданием файлов ответьте на три вопроса:
- что запускает код: URL, событие Drupal, cron, сохранение сущности или вызов из другого компонента;
- нужна ли одна реализация или администратор и разработчики будут выбирать между несколькими;
- должен ли код использоваться повторно и тестироваться отдельно от Drupal-контекста.
Эти ответы важнее размера задачи. Для реакции на создание материала не нужен контроллер. Для отдельной страницы не стоит помещать логику в хук. Для одной операции не нужен plugin manager только потому, что плагины выглядят архитектурно солиднее.
Правильный модуль начинается не с файла
.module, а с ответа на вопрос: кто и когда вызывает ваш код.
Термин. Сервис — объект, зарегистрированный в контейнере зависимостей Drupal. Его можно получить по имени, передать ему зависимости через конструктор и заменить в тесте или другой конфигурации.
Четыре подхода и их цена
| Подход | Когда подходит | Что выигрываете | Чем платите |
|---|---|---|---|
| Хук | Нужно подключиться к заранее известному этапу Drupal | Минимум файлов, быстрый старт | Логика привязана к процедурной точке расширения |
| Маршрут и контроллер | Нужна страница или HTTP-ответ | Понятный URL, права доступа, отдельный обработчик | Контроллер легко перегрузить бизнес-логикой |
| Сервис | Операцию вызывают несколько компонентов | Повторное использование и тестируемость | Появляются YAML и классы |
| Плагин | Есть несколько реализаций одного контракта | Расширяемость без переписывания вызывающего кода | Понадобятся plugin manager, определения и больше соглашений |
Хук: короткий путь для одной реакции
Хук выбирайте, когда Drupal уже предоставляет событие с нужными данными, а действие не требуется вызывать из других мест. Например, hook_help() добавляет справочный текст для модуля, а hook_cron() запускает периодическую работу. Имя функции строится из машинного имени модуля: для site_notice это будет site_notice_cron().
Файл site_notice.module может выглядеть так:
<?php
/**
* Implements hook_cron().
*/
function site_notice_cron(): void {
\Drupal::logger('site_notice')->notice('Периодическая обработка запущена.');
}
Это рабочий вариант для небольшой реакции. Но статический вызов \Drupal::logger() связывает функцию с глобальным контейнером. Если внутри появятся запросы к базе, настройки, HTTP-клиент или сложные правила, хук лучше оставить тонким и передать работу сервису.
Когда хук начинает мешать
Проблема возникает, когда в одном обработчике смешиваются проверка условий, загрузка сущностей, запись данных и отправка уведомлений. Такой код трудно вызвать вручную и проверить изолированно. Хук должен определить момент запуска и передать управление компоненту, который отвечает за действие.
Контроллер: только вход для маршрута
Если пользователь должен открыть адрес вроде /site-notice, используйте маршрут и контроллер. Контроллер принимает запрос, вызывает сервис и возвращает render array — структуру, которую Drupal затем превращает в HTML.
Минимальный каркас модуля:
site_notice.info.yml
site_notice.routing.yml
site_notice.services.yml
src/Controller/NoticeController.php
src/Service/NoticeBuilder.php
Файл site_notice.routing.yml:
site_notice.page:
path: '/site-notice'
defaults:
_controller: '\Drupal\site_notice\Controller\NoticeController::page'
_title: 'Site notice'
requirements:
_permission: 'access content'
Сервис формирует данные, но не знает о маршруте:
<?php
namespace Drupal\site_notice\Service;
final class NoticeBuilder {
public function build(): array {
return [
'#theme' => 'item_list',
'#title' => 'Проверка модуля',
'#items' => [
'Страница работает.',
'Текст собран отдельным сервисом.',
],
];
}
}
Зарегистрируйте его в site_notice.services.yml:
services:
site_notice.notice_builder:
class: Drupal\site_notice\Service\NoticeBuilder
Контроллер получает сервис через внедрение зависимости:
<?php
namespace Drupal\site_notice\Controller;
use Drupal\site_notice\Service\NoticeBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Controller\ControllerBase;
final class NoticeController extends ControllerBase {
public function __construct(
private readonly NoticeBuilder $noticeBuilder,
) {}
public static function create(ContainerInterface $container): static {
return new static(
$container->get('site_notice.notice_builder'),
);
}
public function page(): array {
return $this->noticeBuilder->build();
}
}
После включения модуля очистите кеш Drupal. Затем откройте /site-notice: если права пользователя позволяют доступ к содержимому, появится список из двух пунктов. В рабочем проекте заголовок маршрута и текст интерфейса стоит локализовать, но это отдельная задача от выбора архитектуры.
Сервис: основной кандидат для бизнес-логики
Сервис нужен не ради количества файлов. Его преимущество появляется, когда одна операция вызывается из контроллера, хука, формы или очереди. В конструктор можно передать, например, менеджер сущностей, конфигурацию или логгер. Код не ищет зависимости сам и не знает, откуда его вызвали.
Практическое правило простое:
- Оставьте в контроллере проверку входа и возврат render array.
- Вынесите расчёты, выбор сущностей и изменение данных в сервис.
- Передавайте зависимости в конструктор, а не получайте их через глобальный вызов внутри метода.
- Если сервис возвращает данные, не смешивайте их с HTML без необходимости.
Цена такого решения — дополнительная настройка контейнера и необходимость следить за именами сервисов. Но эта цена быстро окупается, когда операция появляется во втором месте или её нужно проверить без полноценного HTTP-запроса.
Плагин: только если реализаций несколько
Плагин — объект, который Drupal может найти по идентификатору и создать через специальный менеджер. Этот подход оправдан, когда система должна работать с несколькими вариантами одного поведения: например, с разными обработчиками формата данных или способами доставки.
Если у вас только один обработчик и не запланирована расширяемая точка для других модулей, плагин обычно дороже сервиса. Придётся определить контракт, способ хранения метаданных, менеджер и правила выбора. Само слово «плагин» не делает модуль гибче: гибкость появляется только при наличии реальных альтернатив.
Плагин против сервиса
Сервис отвечает на вопрос «как вызвать конкретную операцию». Плагин отвечает на вопрос «какую из нескольких реализаций выбрать». Эти конструкции можно сочетать: менеджер находит плагин, а сам плагин использует сервисы для работы с базой или внешней системой.
Что выбрать в типичных задачах
- Добавить реакцию на cron или событие. Начните с хука, но вынесите сложную работу в сервис.
- Создать страницу администратора или публичный URL. Используйте маршрут и контроллер. Логику контроллеру не оставляйте.
- Переиспользовать расчёт в нескольких местах. Создайте сервис и вызывайте его из разных входов.
- Поддержать несколько алгоритмов. Рассмотрите плагины, если выбор алгоритма действительно нужен и у него есть общий контракт.
- Добавить поле, форматтер или виджет. Сначала проверьте специализированный plugin API Drupal: для таких компонентов общий сервис будет неправильным уровнем расширения.
Главный критерий — не размер класса, а число и тип входов. Один вход и одна реакция допускают хук. Несколько входов ведут к сервису. Несколько взаимозаменяемых реализаций — к плагину.
Что ломается в реальном проекте
Класс не находится
Проверьте машинное имя модуля, namespace и регистр каталогов. Класс Drupal\site_notice\Service\NoticeBuilder должен находиться в src/Service/NoticeBuilder.php. После изменения YAML или добавления класса очистите кеш контейнера.
Маршрут отдаёт 404
Убедитесь, что модуль включён, YAML использует пробелы, а путь начинается с косой черты. Затем очистите кеш маршрутов. Если URL открывается только у администратора, проверьте permission в блоке requirements.
Drupal сообщает о неизвестном сервисе
Имя в $container->get() должно полностью совпадать с ключом в services.yml. После изменения файла сервисов очистите кеш. Ошибка в namespace класса даст другую проблему — контейнер не сможет создать объект.
Контроллер стал «комбайном»
Если в методе маршрута появились запросы к сущностям, бизнес-правила и несколько веток сохранения, вынесите операции в сервис. Контроллер должен оставаться адаптером между HTTP и внутренней логикой.
Что делать дальше
Создайте минимальный модуль на Drupal 10.3–11.x, начните с одного маршрута и одного сервиса, затем добавьте второй вход только при реальной необходимости. После этого проверьте кеширование, права доступа, перевод интерфейса и поведение под анонимным пользователем. В продакшене отдельно наблюдайте логи модуля и время выполнения операции: именно там станет видно, нужна ли очередь, кеш или иной способ расширения.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: Creating modules — Drupal.org, Services and dependency injection in Drupal
Частые вопросы
Можно ли весь модуль написать в файле .module?
Когда вместо сервиса нужен плагин?
Почему после изменения services.yml Drupal не видит сервис?
Можно ли вызывать сервис из хука?
Что делать, если пример не работает на другой версии Drupal?
Есть ли подводные камни у такой архитектуры в продакшене?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.