Построение модуля Drupal 8 – управление конфигурацией и контейнер служб

Эта статья является третьей из трех частей серии о том, как построить модуль Drupal 8.

Как построить модуль Drupal 8:

Построение модуля Drupal 8: маршрутизация, контроллеры и меню ссылок
Построение модуля Drupal 8: блоки и формы
• Построение модуля Drupal 8: управление конфигурацией и контейнер служб

В предыдущей статье, мы рассмотрели создание типов блоков и форм.

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

Аналогично, функция, генерирующая формы, также заключена в одном классе с методами, выполняющими задачи, аналогичные тем, к которым мы привыкли в Drupal 7.

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

Не забывайте, что, если вам нужен будет весь код, который мы создаем по ходу данной серии статей, вы можете посмотреть это хранилище.

Содержание

Конфигурация формы

Когда мы впервые определили нашу DemoForm, мы расширили класс FormBase, который является простейшей реализацией FormInterface. Тем не менее, в Drupal 8 также будет реализована ConfigFormBase, которая обеспечивает дополнительный функционал, что позволяет ему легко взаимодействовать с системой конфигурации.

Теперь мы собираемся преобразовать форму DemoForm в форму, которая будет использоваться для хранения адреса электронной почты, который вводит пользователь. Первое, что мы должны сделать, это заменить расширенный класс на ConfigFormBase (и, конечно, применить его):

use DrupalCoreFormConfigFormBase;

class DemoForm extends ConfigFormBase {

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

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

Через пользовательский интерфейс мы можем изменить эту конфигурацию, после чего мы можем легко экспортировать ее в YAML файлы для развертывания на разных сайтах.

Модуль может задавать в YAML-файле конфигурацию по умолчанию. Файл находится в папке config/install корневой директории модуля. Конвенция назначения имен предполагает добавление к имени файла префикса с названием модуля. Давайте создадим файл demo.settings.yml. И добавим в него следующий код:

demo:
  email_address: demo@demo.com

Это вложенная структура (наподобие ассоциированного массива в PHP). Ниже ключа demo, мы размещаем другую пару ключ/значение. И, как правило, чтобы получить доступ к этим вложенным значениям мы используем точку (.). В нашем случае demo.email_address.

Главное, что вы должны помнить — этот файл импортируется только тогда, когда установлен модуль. Поэтому, после того, как мы создали этот файл, нам нужно переустановить модуль. И теперь мы можем вернуться к форме и настроить методы.

Вот, как должен выглядеть метод buildForm():

public function buildForm(array $form, array &$form_state) {

  $form = parent::buildForm($form, $form_state);

  $config = $this->config('demo.settings');

  $form['email'] = array(
    '#type' => 'email',
    '#title' => $this->t('Your .com email address.'),
    '#default_value' => $config->get('demo.email_address')
  );

  return $form;
}

Прежде всего, в отличие от FormBase, класс ConfigFormBase реализует этот метод, а также добавляет элементы в массив формы (кнопка подтверждения). Так что, мы можем использовать элементы родительского класса, прежде чем добавить собственные.

Теперь что касается конфигурации. Drupal 8 предоставляет объект Config, который мы используем для взаимодействия с конфигурацией. В некоторых классах он уже подключен через введение зависимостей. ConfigFormBase является одним из таких классов.

Как видите, мы используем метод config() родительского класса для извлечения объекта Config, в котором задана наша простая конфигурация demo.settings. Затем для значения #default_value элемента электронной почты, мы используем метод get() объекта Config, чтобы получить значение адреса электронной почты.

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

public function submitForm(array &$form, array &$form_state) {

  $config = $this->config('demo.settings');
  $config->set('demo.email_address', $form_state['values']['email']);
  $config->save();

  return parent::submitForm($form, $form_state);
}

В этом методе мы сначала извлекаем для нашей конфигурации объект Config (так, как мы делали это раньше). Затем, мы используем его метод set(), чтобы изменить значение email_address на значение, которое будет вводить пользователь.

Далее мы используем метод save() для сохранения конфигурации.

И в конце, мы выражаем родительский обработчик предоставления данных, потому что он содержит некоторые необходимые функции (в данном случае он устанавливает в Drupal вывод сообщения на экран).

Это почти все. Вы можете очистить кэш и проверить форму. Отправляя новый адрес электронной почты, вы сохраняете его в конфигурации. Файл модуля demo.settings.yml, конечно, не изменяется, но вы можете продолжить и экспортировать конфигурацию demo.settings, а затем импортировать ее в другом месте.

Контейнер служб и введение зависимостей

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

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

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

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

Сейчас мы создадим очень простую службу и зарегистрируем ее с контейнером. Он будет иметь только один реальный метод, возвращающий простое значение. Затем мы введем эту службу, в качестве зависимости в наш DemoController и используем предоставленное службой значение.

Для того чтобы зарегистрировать службу, мы должны создать файл demo.services.yml, расположенный в корневом каталоге нашего модуля, со следующим содержимым:

services:
    demo.demo_service:
        class: DrupaldemoDemoService

Имя файла задается по шаблону module_name.services.yml.

В первой строке создается массив служб. Вторая строка определяет первую службу (вызываемую файлом demo_service, с префиксом имени модуля). Третья строка определяет класс, который будет создан для этой службы. Из этого следует, что создается файл класса DemoService.php в папке src/ нашего модуля.

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

<?php

/**
 * @file
 * Contains DrupaldemoDemoService.
 */

namespace Drupaldemo;

class DemoService {

  protected $demo_value;

  public function __construct() {
    $this->demo_value = 'Upchuk';
  }

  public function getDemoValue() {
    return $this->demo_value;
  }

}

Я не буду подробно объяснять этот код, поскольку он носит общий характер. Далее давайте вернемся к нашему DemoController и используем эту службу.

Есть два способа, с помощью которых мы можем это сделать: получить доступ к контейнеру глобально через класс Drupal или использовать введение зависимостей, чтобы передать объект этого класса в наш контроллер.

Более правильно будет использовать второй способ, именно это я и собираюсь сделать. Но в некоторых случаях вам нужно будет получить доступ к службе глобально. Для этого, вы можете сделать что-то вроде этого:

$service = Drupal::service('demo.demo_service');

И теперь $service является объектом класса DemoService, который мы только что создали. Но давайте посмотрим, как вводить нашу службу в класс DemoController, в качестве зависимости. Я покажу, что вы должны сделать сначала, а затем вы увидите весь контроллер со всеми изменениями, внесенными в него.

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

С другой стороны, наш контроллер может реализовать ContainerInjectionInterface, который также дает нам доступ к контейнеру. Но мы будем использовать ControllerBase, поэтому нам нужно применить этот класс.

Далее, мы должны применить ContainerInterface из Symfony 2, это требование метода create(), который создает другой объект нашего класса контроллера и передает ему нужную нам службу.

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

Очередность, в которой объекты возвращаются методом create(), должна соответствовать очередности их передачи в конструктор.

Итак, давайте рассмотрим наш измененный DemoController:

<?php

/**
 * @file
 * Contains DrupaldemoControllerDemoController.
 */

namespace DrupaldemoController;

use DrupalCoreControllerControllerBase;
use SymfonyComponentDependencyInjectionContainerInterface;

/**
 * DemoController.
 */
class DemoController extends ControllerBase {

  protected $demoService;

  /**
   * Class constructor.
   */
  public function __construct($demoService) {
    $this->demoService = $demoService;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('demo.demo_service')
    );
  }

  /**
   * Generates an example page.
   */
  public function demo() {
    return array(
      '#markup' => t('Hello @value!', array('@value' => $this->demoService->getDemoValue())),
    );
  }
}

В приведенном выше коде вы можете видеть все описанные этапы. Метод create() создает новый объект нашего класса контроллера, и передает ему нашу службу, извлеченную из контейнера. И в конце, объект класса DemoService сохраняется в свойстве $demoService, и мы можем использовать его, чтобы вызвать его метод getDemoValue().

Это значение затем используется в сообщении «Привет, …!». Очистите кэш и попробуйте перейти по адресу demo/. Вы должны увидеть на странице надпись «Привет, Upchuk

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

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

Заключение

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

Кроме того, вы можете поиграть в пользовательском интерфейсе с импортом/экспортом конфигурации между сайтами. Это позволит вам в будущем усовершенствовать процесс развертывания сайтов.

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

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

Данная публикация представляет собой перевод статьи «Building a Drupal 8 Module – Configuration Management and the Service Container» , подготовленной дружной командой проекта Интернет-технологии.ру