Сервисы и внедрение зависимостей
Сервис — объект, который выполняет одну работу и доступен из любого места сайта: подключение к базе, текущий пользователь, отправка почты. Свою логику тоже оформляют сервисом — тогда один и тот же код используют контроллер, форма, блок и хук.
Класс сервиса
Сервис — обычный класс в папке src модуля. Напишем подсчёт времени чтения статьи. Создайте файл вручную:
// web/modules/custom/blog/src/ReadingTime.php
namespace Drupal\blog;
class ReadingTime
{
public function minutes(string $text): int
{
$words = preg_split('/\s+/u', trim(strip_tags($text)), -1, PREG_SPLIT_NO_EMPTY);
return max(1, (int) ceil(count($words) / 200));
}
}Метод считает слова в тексте и делит на 200 — столько слов в минуту читает человек. От Drupal в классе пока ничего нет.
Регистрация
Чтобы класс стал сервисом, его описывают в файле blog.services.yml в корне папки модуля. Создайте его вручную:
# web/modules/custom/blog/blog.services.yml
services:
blog.reading_time:
class: Drupal\blog\ReadingTimeblog.reading_time — имя сервиса, по нему его получают. После drush cr Drupal знает о сервисе и создаст объект при первом обращении, один на весь запрос.
Получение в контроллере
Класс не создаёт сервис сам, а получает готовый объект через конструктор. Это и называют внедрением зависимостей. В контроллере для этого служит статический метод create():
// web/modules/custom/blog/src/Controller/BlogController.php
namespace Drupal\blog\Controller;
use Drupal\blog\ReadingTime;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
class BlogController extends ControllerBase
{
public function __construct(private ReadingTime $readingTime) {}
public static function create(ContainerInterface $container): static
{
return new static($container->get('blog.reading_time'));
}
public function about(): array
{
$minutes = $this->readingTime->minutes('Блог о разработке на Drupal.');
return ['#markup' => $this->t('Время чтения: @min мин.', ['@min' => $minutes])];
}
}Drupal вызывает create() и передаёт в него контейнер — хранилище всех сервисов. Метод достаёт нужные по имени и отдаёт их конструктору. Страница /blog/about покажет «Время чтения: 1 мин.». Так же устроены формы и блоки.
Получение в хуке
В функциях файла blog.module конструктора нет. Там сервис берут через класс \Drupal:
// web/modules/custom/blog/blog.module
$minutes = \Drupal::service('blog.reading_time')->minutes($text);Сервис с зависимостями
Сервису тоже могут понадобиться другие сервисы. Их перечисляют в arguments; знак @ означает «сервис с таким именем»:
# web/modules/custom/blog/blog.services.yml
services:
blog.reading_time:
class: Drupal\blog\ReadingTime
arguments: ['@config.factory']// web/modules/custom/blog/src/ReadingTime.php
namespace Drupal\blog;
use Drupal\Core\Config\ConfigFactoryInterface;
class ReadingTime
{
public function __construct(private ConfigFactoryInterface $configFactory) {}
public function minutes(string $text): int
{
$speed = $this->configFactory->get('blog.settings')->get('words_per_minute') ?? 200;
$words = preg_split('/\s+/u', trim(strip_tags($text)), -1, PREG_SPLIT_NO_EMPTY);
return max(1, (int) ceil(count($words) / $speed));
}
}Теперь скорость чтения берётся из настроек модуля, а пока их нет — равна 200. Настройки разобраны в уроке о конфигурации. Порядок аргументов в arguments совпадает с порядком в конструкторе.
Частые сервисы ядра
| Имя | Что даёт |
|---|---|
entity_type.manager | Загрузка и сохранение сущностей |
current_user | Текущий пользователь |
config.factory | Настройки |
database | Подключение к базе данных |
messenger | Сообщения посетителю |
logger.factory | Журнал |
http_client | Запросы к другим сайтам |
Частая ошибка
Вызовы \Drupal::service() внутри классов — контроллеров, форм, своих сервисов — скрывают зависимости: по конструктору не видно, что нужно классу, и в тесте его не создать. Статический доступ оставляйте для функций-хуков, а в классах передавайте сервисы через конструктор. И помните: blog.services.yml читается только при drush cr.