Использование Dependency Injection в Drupal 8

Что такое сервисы в Drupal 8?

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

Сервисы и инъекция зависимости в Drupal 8

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

Dependency Injection для класса формы (Drupal\Core\Form\FormBase)

Мы рассмотрим пример внедрения зависимости подключения к базе данных в форму. Весьма странно, что эта зависимость не внедрена «из коробки», поскольку при работе с формами обращение к базе данных необходимо практически всегда. Вообще, официальное руководство рекомендует проверять, не существует ли нужная зависимость уже как член класса, например подключение к БД уже присутствует во многих плагинах и сервисах как $this->connection. Но в форме его нет.

Как вы наверняка уже знаете, чтобы создать форму в Drupal 8 необходимо наследовать абстрактный класс Drupal\Core\Form\FormBase и реализовать как минимум три метода:

  • getFormId()
  • buildForm(array $form, FormStateInterface $form_state)
  • submitForm(array &$form, FormStateInterface $form_state)

Желательно (если в этом есть необходимость) переопределить уже существующий но пустой метод validateForm(array &$form, FormStateInterface $form_state). Также необходимо в файле my_module.routing.yml добавить путь (роут) по которому будет доступна наша форма.

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

Если бы мы писали в процедурном стиле, то получить объект подключения к БД можно одним из двух способов:

$connection = \Drupal::database();

или

$connection = \Drupal::service('database');

В результате в переменной $connection мы будем иметь объект подключения, настроенный на подключение к основной БД, указанной в settings.php.

Но если придерживаться ООП стиля, рекомендуется использовать Dependency Injection. Для этого создадим в нашем классе защищенное свойство $dbConnection, передадим объект подключения в конструктор, и все что наш конструктор будет делать — инициализировать внутреннюю переменную $dbConnection, присваивая ей полученный в параметрах объект подключения. Классически инъекция зависимости так и работает. Но если все так и оставить, мы получим в логах сообщение об ошибке типа:

ArgumentCountError: Too few arguments to function Drupal\my_module\Form\MyForm::__construct(), 0 passed in ... /my_module/web/core/lib/Drupal/Core/Form/FormBase.php on line ... and exactly 1 expected in Drupal\my_module\Form\MyForm->__construct()

Дело в том, что в случае с формами (классы Drupal\Core\Form\FormBase, Drupal\Core\Form\ConfigFormBase, Drupal\Core\Form\ConfirmFormBase), и многими другими компонентами Drupal, инъекция зависимости производится через статический метод create. Как указано в комментариях в классе формы (например Drupal\Core\Form\FormBase), «To properly inject services, override create() and use the setters provided by the traits to inject the needed services.» Описывая метод create() мы реализуем ContainerInjectionInterface (Drupal\Core\DependencyInjection\ContainerInjectionInterface).

Поэтому просто добавляем статический метод create, который принимает параметром контейнер сервисов $container, извлекает из него сервис «database», и возвращает новый экземпляр класса с уже внедренной зависимостью.

Код:

namespace Drupal\my_module\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Database\Connection;
use Symfony\Component\DependencyInjection\ContainerInterface;

class MyForm extends FormBase {
  /**
   * @var \Drupal\Core\Database\Connection $dbConnection
   */
  protected $dbConnection;

  /**
   * Constructs a new MyForm object.
   * @param \Drupal\Core\Database\Connection $dbConnection
   */
  public function __construct(Connection $dbConnection) {
    $this->dbConnection = $dbConnection;
  }

  public static function create(ContainerInterface $container) {
  /**
   * @var \Drupal\Core\Database\Connection $dbConnection
   */
    $dbConnection = $container->get('database');
    return new static($dbConnection);
  }

  public function getFormId() {
  }

  public function buildForm(array $form, FormStateInterface $form_state) {
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {
  $name = 'abc';
  $addNewRecordResult = $this->dbConnection->insert('my_module_table')
    ->fields([
    'uid' => $this->currentUser()->id(),
    'created' => time(),
    'name' => $name,
    ])
  ->execute();
  }
}

Dependency Injection для своего класса

Теперь рассмотрим случай внедрения зависимости в свой собственный класс. Здесь есть некоторые отличия. Допустим, мы хотим создать свой класс-обертку для объекта по работе с БД (в нашем примере это $dbConnection), чтобы написать свои собственные методы под конкретную задачу. Создаем класс (DatabaseManager):

namespace Drupal\my_module\CoreClasses\DatabaseManager;

use Drupal\Core\Database\Connection;

class DatabaseManager {
  protected $mappedData = null;
  protected $selectedTable = null;

  /**
   * @var \Drupal\Core\Database\Connection $dbConnection
   */
  protected $dbConnection;

  /**
   * Constructs a new DatabaseManager object.
   * @param \Drupal\Core\Database\Connection $dbConnection
   */
  public function __construct(Connection $dbConnection) {
    $this->dbConnection = $dbConnection;
  }

  public function setData($mappedData) {
    $this->mappedData = $mappedData;
    return $this;
  }

  public function selectTable($tableName) {
    $this->selectedTable = $tableName;
    return $this;
  }

  public function insertRecord() {
    $this->dbConnection->insert($this->selectedTable)->fields($this->mappedData)->execute();
  }

  public function checkIfRecordExists() {
  }
}

Наш класс не обязан реализовывать ContainerInjectionInterface описывая статический метод create() — этого метода в нашем классе нет (в отличие от класса формы, где он есть изначально, и мы должны его переопределить). Объекты нашего класса DatabaseManager будут инстанциироваться через конструктор, который принимает объект подключения к БД как параметр. Но так как мы не хотим создавать экземпляр класса вручную, а хотим чтобы об этом позаботился Drupal через Dependency Injection, мы должны уведомить Drupal о зависимостях для нашего класса. Для этого нужно зарегистрировать свой класс в контейнере сервисов, и указать зависимости, экземпляры которых будут переданы в конструктор нашего класса. Делается это с помощью yml файла, который следует назвать типа my_module.services.yml и разместить в корневой папке нашего модуля:

services:
  my_module.database_manager:
    class: Drupal\my_module\CoreClasses\DatabaseManager\DatabaseManager
    arguments: ['@database']

В этом файле указано:

  • Название нашего нового сервиса (my_module.database_manager);
  • Путь в пространстве имен к классу нашего сервиса (Drupal\my_module\CoreClasses\DatabaseManager\DatabaseManager);
  • Имена других сервисов от которых зависит наш сервис. В данном случае наш сервис зависит от системного database. Знак @ используется чтобы указать Drupal что аргумент является сервисом. Если опустить @ аргумент будет рассматриваться как простая строка. Подробнее в документации.

Теперь, когда где-либо в коде будет запрашиваться «my_module.database_manager», контейнер сервисов гарантирует, что сервис «database» будет передан в конструктор нашего сервиса, запросив его (а также другие сервисы, которые указаны в зависимостях), и передав в указанной последовательности в конструктор сервиса my_module.database_manager.

Будьте аккуратны с отступами в файлах yml, так как отступы указывают логическую вложенность, и если составить yml файл без отступов, или с неправильными отступами, работать он не будет.

Теперь, когда мы оформили наш класс в виде сервиса, и даже внедрили в него зависимости от системных сервисов (database), самое время воспользоваться новой функциональностью. Как? Опять-таки через Dependency Injection. Вообще, это хорошая практика для Drupal 8 — оформляем свой функционал в виде сервиса и внедряем его через DI.

Класс формы, в которой используется наша кастомная обертка DatabaseManager будет выглядеть так:

namespace Drupal\my_module\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\sherlock_d8\CoreClasses\DatabaseManager\DatabaseManager;

class MyForm extends FormBase {
  /**
   * @var \Drupal\my_module\CoreClasses\DatabaseManager\DatabaseManager $dbConnection
   */
  protected $dbConnection;

  /**
   * Constructs a new MyForm object.
   * @param \Drupal\sherlock_d8\CoreClasses\DatabaseManager\DatabaseManager $dbConnection
   */
  public function __construct(DatabaseManager $dbConnection) {
    $this->dbConnection = $dbConnection;
  }

  /**
   * Overriding of static method create() required to properly implement ContainerInjectionInterface
   * @param ContainerInterface $container
   * @return DatabaseManager
   */
  public static function create(ContainerInterface $container) {
  /**
   * @var \Drupal\sherlock_d8\CoreClasses\DatabaseManager\DatabaseManager $dbConnection
   */
    $dbConnection = $container->get('my_module.database_manager');
    return new static($dbConnection);
  }

  public function getFormId() {
  }

  public function buildForm(array $form, FormStateInterface $form_state) {
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {
    $name = 'abc';
    $dataToInsert = [
      'uid' => $this->currentUser()->id(),
      'created' => time(),
      'name' => $searchName,
    ];

    $this->dbConnection->setData($dataToInsert)->selectTable('my_module_table')->insertRecord();
  }
}

Как видим, как и в самом первом случае с формой, для класса формы приходится переопределять метод create() (Drupal\Core\DependencyInjection\ContainerInjectionInterface::create()), поскольку инъекция зависимости в данном случае происходит именно через него.

Для собственного класса метод create() не обязателен, вместо этого создаем yml-файл, в котором указываем какие зависимости следует передать в конструктор.

Получить объект собственного сервиса (как и любого системного) можно и в процедурном стиле:

$dbc = \Drupal::service('my_module.database_manager');

Но если есть возможность, лучше это делать через инъекцию зависимости.

Оставьте первый комментарий

Оставить комментарий