Как я уверен, вы уже знаете, что внедрение зависимостей (DI) и сервисный контейнер Symfony являются важными новыми возможностями разработки Drupal 8. Однако, хотя они начинают лучше понимать в сообществе разработчиков Drupal, все еще есть некоторые недостатки. ясности о том, как именно внедрить сервисы в классы Drupal 8.
Многие примеры говорят об услугах, но большинство покрывают только статический способ их загрузки:
1
|
$service = \Drupal::service(‘service_name’);
|
Это понятно, так как правильный подход к инъекциям является более многословным, и, если вы уже знаете это, скорее шаблонный. Однако статический подход в реальной жизни следует использовать только в двух случаях:
- в файле
.module
(вне контекста класса) - те редкие случаи в контексте класса, когда класс загружается без уведомления контейнера службы
Помимо этого, внедрение служб инъекций является наилучшей практикой, поскольку оно обеспечивает разделенный код и облегчает тестирование.
В Drupal 8 есть некоторые особенности внедрения зависимостей, которые вы не сможете понять исключительно из чистого подхода Symfony. Итак, в этой статье мы рассмотрим некоторые примеры правильного внедрения конструктора в Drupal 8. С этой целью, а также для охвата всех основ, мы рассмотрим три типа примеров в порядке сложности:
- впрыскивать услуги в другую из ваших собственных услуг
- внедрение услуг в не обслуживающие классы
- внедрение услуг в классы плагинов
В дальнейшем предполагается, что вы уже знаете, что такое DI, для какой цели он служит и как его поддерживает сервисный контейнер. Если нет, я рекомендую сначала проверить эту статью .
Сервисы
Внедрить услуги в ваш собственный сервис очень просто. Так как вы определяете сервис, все, что вам нужно сделать, это передать его в качестве аргумента сервису, который вы хотите внедрить. Представьте себе следующие определения услуг:
1
2
3
4
5
6
|
services:
demo.demo_service:
class: Drupal\demo\DemoService
demo.another_demo_service:
class: Drupal\demo\AnotherDemoService
arguments: [‘@demo.demo_service’]
|
Здесь мы определяем два сервиса, где второй принимает первый в качестве аргумента конструктора. Таким образом, все, что нам теперь нужно сделать в классе AnotherDemoService
это сохранить его как локальную переменную:
01
02
03
04
05
06
07
08
09
10
11
12
|
class AnotherDemoService {
/**
* @var \Drupal\demo\DemoService
*/
private $demoService;
public function __construct(DemoService $demoService) {
$this->demoService = $demoService;
}
// The rest of your methods
}
|
И это в значительной степени это. Также важно отметить, что этот подход точно такой же, как в Symfony, поэтому здесь никаких изменений нет.
Необслуживаемые классы
Теперь давайте посмотрим на классы, с которыми мы часто взаимодействуем, но которые не являются нашими собственными сервисами. Чтобы понять, как происходит это внедрение, вам необходимо понять, как решаются классы и как они создаются. Но скоро мы увидим это на практике.
Контроллеры
Классы контроллеров в основном используются для отображения путей маршрутизации к бизнес-логике. Предполагается, что они остаются слабыми и делегируют более тяжелую бизнес-логику сервисам. Многие расширяют класс ControllerBase
и получают некоторые вспомогательные методы для извлечения общих служб из контейнера. Однако они возвращаются статически .
Когда создается объект ControllerResolver::createController
( ControllerResolver::createController
), ClassResolver
используется для получения экземпляра определения класса контроллера. Средство распознавания распознает контейнер и возвращает экземпляр контроллера, если он уже есть в контейнере. И наоборот, он создает новый экземпляр и возвращает его.
И здесь происходит наше внедрение: если разрешаемый класс реализует ContainerAwareInterface
, создание экземпляра происходит с помощью статического метода create()
для этого класса, который получает весь контейнер. И наш класс ControllerBase
также реализует ContainerAwareInterface
.
Итак, давайте посмотрим на пример контроллера, который правильно внедряет сервисы, используя этот подход (вместо того, чтобы запрашивать их статически):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
/**
* Defines a controller to list blocks.
*/
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()
и constructor, вы можете внедрить все, что захотите. Если вы не хотите расширять эти классы, все, что вам нужно сделать, это реализовать этот интерфейс самостоятельно и выполнить те же шаги, которые мы видели выше с контроллером.
В качестве примера, давайте посмотрим на SiteInformationForm
которая расширяет ConfigFormBase
и посмотрим, как он внедряет сервисы поверх config.factory
в котором config.factory
его родительский config.factory
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
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 8. Она доступна почти во всех контекстах классов, за исключением тех, в которых часть создания экземпляра еще не была решена таким образом (например, плагины FieldType). Кроме того, существует важная подсистема, которая имеет некоторые отличия, но крайне важна для понимания: плагины.
Плагины
Система плагинов является очень важным компонентом Drupal 8, который обеспечивает большую функциональность. Итак, давайте посмотрим, как внедрение зависимостей работает с классами плагинов.
Самое важное различие в том, как инъекция обрабатывается с помощью плагинов, заключается в реализации интерфейсных классов плагинов: ContainerFactoryPluginInterface
. Причина в том, что плагины не разрешаются, а управляются менеджером плагинов. Поэтому, когда этот менеджер должен создать экземпляр одного из своих плагинов, он сделает это с помощью фабрики. И обычно, эта фабрика — это ContainerFactory
(или аналогичный вариант).
Поэтому, если мы посмотрим на ContainerFactory::createInstance()
, мы увидим, что помимо контейнера, передаваемого обычному методу create()
, также $plugin_definition
переменные $configuration
, $plugin_id
и $plugin_definition
(которые являются тремя основными параметры каждого плагина поставляется с).
Итак, давайте рассмотрим два примера таких плагинов, которые внедряют сервисы. Во-первых, основной плагин @Block
( @Block
):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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()
получает эти три дополнительных параметра. Затем они передаются в правильном порядке конструктору класса, а из контейнера запрашивается и также передается служба. Это самый простой, но часто используемый пример внедрения сервисов в классы плагинов.
Еще один интересный пример — плагин @FieldWidget
( @FieldWidget
):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
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 8. Иногда нам приходится статически запрашивать их. Однако большую часть времени мы не должны. И мы видели несколько типичных примеров того, когда и как мы должны внедрять их в наши классы. Мы также видели два основных интерфейса, которые классы должны реализовать, чтобы создать экземпляр с контейнером и быть готовыми к внедрению, а также разницу между ними.
Если вы работаете в контексте класса и не знаете, как внедрять службы, начните смотреть на другие классы этого типа. Если это плагины, проверьте, реализует ли кто-либо из родителей объект ContainerFactoryPluginInterface
. Если нет, сделайте это сами для своего класса и убедитесь, что конструктор получает то, что ожидает. Также проверьте ответственный класс менеджера плагинов и посмотрите, какую фабрику он использует.
В других случаях, таких как классы FieldType
, такие как FieldType
, взгляните на другие примеры в ядре. Если вы видите, что другие используют статически загруженные сервисы, скорее всего, они еще не готовы к внедрению, поэтому вам придется сделать то же самое. Но следите, потому что это может измениться в будущем.