Статьи

Drupal 8: правильное внедрение зависимостей с помощью DI

Как я уверен, вы уже знаете, что внедрение зависимостей (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 , взгляните на другие примеры в ядре. Если вы видите, что другие используют статически загруженные сервисы, скорее всего, они еще не готовы к внедрению, поэтому вам придется сделать то же самое. Но следите, потому что это может измениться в будущем.