03/07/2021, by Ivan

Когда мы используем Drupal и нам нужно использовать код контрибного модуля или модуля ядра в кастомном модуле, то мы используем hook'и и services (сервисы). Мы уже использовали хуки в этой статье:

12.11.3. Хуки для работы с Entity.

Давайте теперь разберемся с services. Service - это PHP объект. Поэтому когда вы создаете новый PHP класс в вашем кастомном модулей, то лучше сразу оформлять его для service, чтобы потом ваш код можно было использовать в другом модуле стандартным способом.

Drupal собирает все services в PHP объекте Service Container, так что друпал хранит информацию обо всех доступных и используемых сервисах в одном месте. Вы можете вызвать этот объект и посмотреть какие сервисы у вас используются:

<?php
$container = \Drupal::getContainer();
?>

https://api.drupal.org/api/drupal/core!lib!Drupal.php/function/Drupal%3A%3AgetContainer/9.2.x

Get container

Service container

Вы можете работать с этим объектом через has/get/set методы, но обычно мы будем добавлять сервисы в контейнер при помощи *.services.yml файлы в наших модулях.

Давайте посмотрим на реализацию метода getContainer():

<?php
public static function getContainer() {
  if (static::$container === NULL) {
    throw new ContainerNotInitializedException('\\Drupal::$container is not initialized yet. \\Drupal::setContainer() must be called with a real container.');
  }
  return static::$container;
}
?>

Переменная Service контейнера определенна как статическая, это означает, что после вызова index.php и до конца обработки вызова в любом месте мы можем получить значение этой переменной в любом файле по ходу вызова: это может быть любой класс или хук в модуле или даже .theme файл темы.

Как использовать сервисы в Drupal?

Теперь давайте перейдем к тому как использовать Service Container в Drupal. В объекте $container хранятся объекты services, что позволяет выполнить всю необходимую логику для создания объекта в конструкторе и передать нам уже готовый к использованию объект в наш кастомный модуль. Например нам нужно написать написать SQL запрос к базе, мы просто вызываем объект для работы с базой данных из Service контейнера и этот объект для работы с базой данных уже использовал credentials из нашего settings.php файла и установит соединение с MySQL при выполнение нашего SQL запроса:

$query = \Drupal::database()->select('node_field_data', 'n');
$query->addField('n', 'nid');
$query->condition('n.title', 'About Us');
$query->range(0, 1);
$nid = $query->execute()->fetchField();

 Если вы посмотрите в реализацию метода database(), то увидите, что мы используем объект сервиса database из Service контейнера:

https://api.drupal.org/api/drupal/core%21lib%21Drupal.php/function/Drupal%3A%3Adatabase/9.2.x

<?php
public static function database() {
  return static::getContainer()
    ->get('database');
}
?>

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

Как добавить сервис в Service container?

Когда мы создаем файл *.services.yml, то Drupal подгружает сервисы из этих файлов и сохраняет объекты из них в Service container.

https://api.drupal.org/api/drupal/core%21modules%21syslog%21syslog.services.yml/9.2.x 

core/modules/syslog/syslog.services.yml:

services:
  logger.syslog:
    class: Drupal\syslog\Logger\SysLog
    arguments: ['@config.factory', '@logger.log_message_parser']
    tags:
      - { name: logger }

В переменную $container можно добавить сервис через метод set(), но обычно это используется чтобы мокать (mocking) зависимости в тестах:

https://www.drupal.org/docs/automated-testing/phpunit-in-drupal/mocking-entities-and-services-with-phpunit-and-mocks

Что такое Dependency Injection?

Если вы запустите Code Sniffer, то он выдаст вам ошибку, что Drupal::database() нужно поправить и вызвать database в конструкторе класса в котором мы используем объект из Service контейнера. Когда вы вызываете объект из Service Container в конструкторе класса это и называется Dependency Injection (DI), например:

<?php

namespace Drupal\wisenet_connect\Form;

use Drupal\Core\Database\Connection;

/**
 * Implements the WisenetConfigurationForm form controller.
 *
 * This example demonstrates a simple form with a singe text input element. We
 * extend FormBase which is the simplest form base class used in Drupal.
 *
 * @see \Drupal\Core\Form\FormBase
 */
class WisenetGetCourseForm extends FormBase {

  /**
   * Active database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;


  /**
   * Constructs a WisenetGetCourseForm object.
   *
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection to be used.
   */
  public function __construct(Connection $database) {
    $this->database = $database;
  }

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

 ...

  /**
   * Implements course save handler.
   *
   * Function for save course data in course content type.
   */
  public function saveCourse($courses) {
     ...
      $query = $this->database->select('node__field_course_wisenet_id', 'nc');
      $query->addField('n', 'nid');
      $query->join('node_field_data', 'n', 'nc.entity_id = n.nid');
      $query->condition('nc.field_course_wisenet_id_value', $course['CourseOfferId']);
      $query->range(0, 1);

      $nid = $query->execute()->fetchField();
     ...
  }

В данном примере запрос к базе данных нужен в форме, поэтому мы добавили дополнительный метод create(), который используется для создания экземпляра класса. create() метод может быть в различных классах и интерфейсах, но всегда у него в параметрах переменная $container типа ContainerInterface. Если в методе create() вызывается объект из Service container $container->get('myservice.name'), то возвращаемый объект будет передан в конструктор __contruct() как аргумент метода (в нашем случае $container->get('database') и аргумент Connection $database).

О том как именно нужно вызывать объекты из Service контейнера в конструкторе контроллера (Controller), блока (Block), формы (BaseForm), конфигурационной формы (ConfigForm) кастомного класса/сервиса мы размотрим в следующих статьях.

После того как мы рассмотрим как подключать и использовать объекты из Service container правильно, мы рассмотрим как создавать свои сервисы.

Также мы рассмотрим как переопределять классы для сервисов, чтобы вместо класса котрибного модуля использовать класс из кастомного модуля.

Зачем нужны Service container и Dependency Injection?

Мы можем использовать namespaces и подключить код из модулей напрямую, вызывать создание объектов для сторонних классов в том месте где нам нужно без всяких Service container. Но это вызывает проблемы с обновлением кода. Например мы должны заменить класс для отправки email сообщений и этот класс вызывается в 200 разных местах. Для удобства обновления кода мы создаем Service, а не подключение файлов напрямую. Теперь когда мы хотим отправлять email'ы через smtp, а не через PHP mail(), то мы просто изменим класс для сервиса, а не будем менять путь к новому классу в 200 местах. 

Dependency Injection решает проблему с двойным вызовом сервиса в одном классе. Нам не нужно обращаться дважды к Service container, если мы используем сервис в разных методах одного класса. Мы просто записываем объект сервиса в свойство нашего класса и используем сервис уже из свойства класса с помощью $this->serviceName.

Конечно, мы может обойтись без Service container и Dependency Injection, но эти паттерны унифицируют наш код и позволяют упростить его и возможность его обновления.

Где я могу увидеть имя сервиса?

В нашем примере у нас есть сервис "database":

/**
 * {@inheritdoc}
 */
public static function create(ContainerInterface $container) {
  return new static(
    // Here we add service name.
    $container->get('database'),
  );
}

но если вы добавите сервис из contrib/custom модуля, то он может иметь такое имя:

имя_модуля.имя_сервиса

Вы можете проверить имя сервиса в файле * .services.yml. Имя сервиса не обязательно начинается с module_name.*, но обычно это так. Например:

/**
 * {@inheritdoc}
 */
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
  return new static(
    $configuration,
    $plugin_id,
    $plugin_definition,
    $container->get('commerce_cart.cart_provider'),
    $container->get('entity_type.manager')
  );
}

 

No war