Найти в Дзене
Topsite Web

Правильное внедрение зависимостей (DI) в Drupal

Оглавление

Внедрение зависимостей (DI) и сервисный контейнер Symfony являются важными новыми функциями разработки Drupal. Однако, несмотря на то, что они начинают лучше пониматься в сообществе разработчиков Drupal, все еще есть некоторые недопонимания относительно того, как именно внедрять сервисы в классы Drupal.

Во многих примерах говорится о сервисах, но большинство из них описывают только статический способ их загрузки:

$service  =  \ Drupal :: service ( 'service_name' );

Это понятно, поскольку правильный подход к внедрению более подробный и если вы это уже знаете, скорее шаблонный. Однако статический подход в реальной жизни следует использовать только в двух случаях:

  1. в .module файле (вне контекста класса)
  2. редкие случаи в контексте класса, когда класс загружается без ведома сервисного контейнера

Помимо этого, внедрение сервисов является лучшей практикой, поскольку оно обеспечивает разделение кода и упрощает тестирование.

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

  • внедрение сервисов в другие ваши собственные сервисы
  • внедрение сервисов в несервисные классы
  • внедрение сервисов в классы плагинов

Сервисы

Внедрить сервисы в свой собственный сервис очень просто. Поскольку вы определяете службу, все что вам нужно сделать, это передать ее в качестве аргумента службе, которую вы хотите внедрить. Представьте себе следующие определения сервисов:

services:
demo.demo_service:
class: Drupal\demo\DemoService
demo.another_demo_service:
class: Drupal\demo\AnotherDemoService
arguments: ['@demo.demo_service']

Здесь мы определяем две службы, вторая принимает первую в качестве аргумента конструктора. Все что нам нужно сделать сейчас в AnotherDemoService классе — это сохранить его как локальную переменную:

class AnotherDemoService {
/**
* @var \Drupal\demo\DemoService
*/
private $demoService;
public function __construct(DemoService $demoService) {
$this->demoService = $demoService;
}
// Остальные ваши методы
}

Важно отметить, что этот подход точно такой как в Symfony, поэтому никаких изменений здесь нет.

Необслуживаемые классы

Посмотрим на классы, с которыми мы часто взаимодействуем, но которые не являются нашими собственными сервисами. Чтобы понять, как происходит это внедрение, вам необходимо понять, как разрешаются классы и как создаются их экземпляры.

Контроллеры

Классы контроллера в основном используются для сопоставления путей маршрутизации с бизнес-логикой. Они должны делегировать более сложную бизнес-логику сервисам. Многие расширяют ControllerBase класс и получают некоторые вспомогательные методы для извлечения общих сервисов из контейнера. Однако они возвращаются статически.

При создании объекта контроллера (ControllerResolver::createController) ClassResolver используется для получения экземпляра определения класса контроллера. Резолвер учитывает контейнер и возвращает экземпляр контроллера, если он уже есть в контейнере. И наоборот, он создает новый экземпляр и возвращает его.

И вот где происходит наша инъекция — если разрешаемый класс реализует ContainerAwareInterface, создание экземпляра происходит с использованием статического create() метода этого класса, который получает весь контейнер. И наш ControllerBase класс также реализует ContainerAwareInterface.

Давайте посмотрим на пример контроллера, который правильно внедряет сервисы, используя этот подход (вместо того, чтобы запрашивать их статически):

/**
* Определяет контроллер для составления списка блоков.
*/
class BlockListController extends EntityListController {
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* Constructs the BlockListController.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
*/
public function __construct(ThemeHandlerInterface $theme_handler) {
$this->themeHandler = $theme_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('theme_handler')
);
}
}

Здесь класс EntityListController ничего не делает для наших целей, поэтому просто представьте, что BlockListController напрямую расширяет ControllerBase класс, который в свою очередь реализует ContainerInjectionInterface.

Как мы уже говорили при создании экземпляра этого контроллера вызывается статический метод create(). Его цель — создать экземпляр этого класса и передать любые параметры конструктору класса. А поскольку контейнер передается в create(), он может выбирать, какие службы запрашивать, и передавать их конструктору.

Затем конструктору просто нужно получить сервисы и сохранить их локально. Имейте в виду, что внедрение всего контейнера в ваш класс — плохая практика и вам всегда следует ограничивать внедряемые сервисы теми, которые вам нужны. И если вам нужно слишком много, вы вероятно делаете что-то неправильно.

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

Формы

Формы — еще один отличный пример классов, в которые необходимо внедрять сервисы. Обычно вы либо расширяете классы FormBase, либо ConfigFormBase уже реализуют ContainerInjectionInterface. В этом случае, если вы переопределите create() методы конструктора и вы сможете внедрить все что захотите. Если вы не хотите расширять эти классы, все что вам нужно сделать, это реализовать этот интерфейс самостоятельно и выполнить те же шаги, которые мы видели выше с контроллером.

В качестве примера давайте взглянем на который SiteInformationForm расширяет ConfigFormBase и посмотрим, как он внедряет сервисы поверх config.factory родительских потребностей:

class SiteInformationForm extends ConfigFormBase {
...
public function __construct(ConfigFactoryInterface $config_factory, AliasManagerInterface $alias_manager, PathValidatorInterface $path_validator, RequestContext $request_context) {
parent::__construct($config_factory);
$this->aliasManager = $alias_manager;
$this->pathValidator = $path_validator;
$this->requestContext = $request_context;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('path.alias_manager'),
$container->get('path.validator'),
$container->get('router.request_context')
);
}
...
}

Как и раньше, create() метод используется для создания экземпляра, который передает конструктору сервис, требуемый родительским классом, а также некоторые дополнительные, необходимые ему сверху.

И примерно так работает базовое внедрение конструктора в Drupal. Оно доступно практически во всех контекстах классов, за исключением некоторых, в которых часть создания экземпляров еще не была решена таким образом (например, плагины FieldType). Кроме того, существует важная подсистема, которая имеет некоторые различия, но которую крайне важно понимать — плагины.

Плагины

Система плагинов — очень важный компонент Drupal, обеспечивающий множество функций. Давайте посмотрим, как внедрение зависимостей работает с классами плагинов.

Наиболее важным отличием в том, как внедрение осуществляется с помощью плагинов, является то, какие классы интерфейсных плагинов должны реализовать ContainerFactoryPluginInterface. Причина в том, что плагины не разрешаются, а управляются менеджером плагинов. Поэтому, когда этому менеджеру необходимо создать экземпляр одного из своих плагинов, он сделает это с помощью фабрики. И обычно это фабрика ContainerFactory(или ее аналог).

Если мы посмотрим на ContainerFactory::createInstance(), мы увидим, что помимо контейнера, передаваемого обычному create() методу, также передаются переменные.

Давайте посмотрим два примера таких плагинов, которые внедряют сервисы. Во-первых, основной UserLoginBlock плагин (@blocks)

class UserLoginBlock extends BlockBase implements ContainerFactoryPluginInterface {
...
public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteMatchInterface $route_match) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('current_route_match')
);
}
...
}

Как видите, он реализует метод ContainerFactoryPluginInterface и create() получает эти три дополнительных параметра. Затем они передаются в правильном порядке конструктору класса, а из контейнера также запрашивается и передается служба. Это самый простой, но часто используемый пример внедрения сервисов в классы плагинов.

Еще один интересный пример — FileWidget плагин ( @FieldWidget):

class FileWidget extends WidgetBase implements ContainerFactoryPluginInterface {
/**
* {@inheritdoc}
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $element_info) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
$this->elementInfo = $element_info;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($plugin_id, $plugin_definition, $configuration['field_definition'], $configuration['settings'], $configuration['third_party_settings'], $container->get('element_info'));
}
...
}

Как видите, create() метод получает те же параметры, но конструктор класса ожидает дополнительные, специфичные для этого типа плагина. Это не проблема. Обычно их можно найти внутри $configuration массива этого конкретного плагина и передать оттуда.

Это основные различия, когда дело доходит до внедрения сервисов в классы плагинов. В методе необходимо реализовать другой интерфейс и некоторые дополнительные параметры create().

Заключение

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

Если вы работаете в контексте класса и не знаете, как внедрять сервисы, начните искать другие классы этого типа. Если это плагины, проверьте, реализует ли кто-нибудь из родителей расширение ContainerFactoryPluginInterface. Если нет, сделайте это самостоятельно для своего класса и убедитесь, что конструктор получает то, что ожидает. Также проверьте ответственный класс менеджера плагинов и посмотрите, какую фабрику он использует.

В других случаях, например, с классами TypedData, такими как FieldType, взгляните на другие примеры в ядре. Если вы видите, что другие используют статически загружаемые сервисы, скорее всего, они еще не готовы к внедрению, поэтому вам придется сделать то же самое.