Урок 5. Сервисы

Drupal 8. Урок 5. Изучаем сервисы

Сервисы — это объекты, которыми управляет сервис контейнер (он же контейнер зависимостей, Dependency injection). По своей сути сервисы как и плагины, представляют собой кусочек функциональности, который выполняет конкретную задачу. Например, отправка писем, получение данных от какого-либо стороннего API, подключение к базе данных, работа с кешем и т.д.
Сервисы определяются в специальном файле *.services.yml. Данный файл в своем имени использует префикс модуля, в котором он определен.

Содержание

Структура файла services.yml

Файл *.services.yml содержит достаточно большое количество свойств. Разберемся, за что каждое из них отвечает[1].

abstract

Свойство указывает на то, что сервис является абстрактный, т.е. не будет возвращать реальный результат. Данный сервис используется как родительский (parent). Доступные варианты значений:

  • true (сервис является абстрактным)
  • false (дефолтное значение, сервис будет возвращать результат)

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

1
2
3
4
5
6
services:
  mymodule.my_service:
    abstract:  true
    arguments: ['@some_argument']
    calls:
      - [someMethod, ['@mymodule.some_service']]

alias

Свойство необходимо, если нужно использовать сокращенное название сервиса[2]. Например, есть сервис

1
2
3
services:
 mymodule.this_name_of_service_is_very_long_name_in_the_world:
   class: Drupal\mymodule\MyService\MyServiceClass

Чтобы обращаться к сервису по короткому имени, нужно объявить другой сервис со свойством alias и указать в его значении имя сервиса с длинным именем, т.е. сделать своего рода "ссылку".

1
2
3
4
5
6
services:
 mymodule.this_name_of_service_is_very_long_name_in_the_world:
   class: Drupal\mymodule\MyService\MyServiceClass
 
 mymodule.short_name:
   alias: mymodule.this_name_of_service_is_very_long_name_in_the_world

Далее, чтобы обратиться к mymodule.this_name_of_service_is_very_long_name_in_the_world используем сокращенное имя

1
$service = \Drupal::service('mymodule.short_name');

arguments

Цель использования аргументов — передать в сервисы дополнительную информацию. Аргументы используются с методом фабрики или с конструктором класса. Символ @ указывает на то, что в качестве аргумента будет использоваться результат другого сервиса. После символа @ указываем имя сервиса, определенного в *.services.yml файле. Например

1
2
3
4
5
6
7
8
9
services:
  #...
  locale.project:
    class: Drupal\locale\LocaleProjectStorage
    arguments: ['@keyvalue']
  #...
  keyvalue:
    class: Drupal\Core\KeyValueStore\KeyValueFactory
    arguments: ['@service_container', '%factory.keyvalue%']

Аргумент, заключенный в символ %, представляет собой параметр, который определен в секции parameters файла *.services.yml. Более детально параметры разберем в пункте Parameters файла services.yml.

calls

Использует setter injection. Определяет дополнительные методы, которые будут вызваны после того как сервис будет инстанцирован[3].
Пример:

1
2
3
4
5
services:
  mymodule.my_service:
    class: Drupal\mymodule\MyService\MyServiceClass
    calls:
      - [someMethod, ['@mymodule.some_service']]

class

Класс сервиса. Содержит основную логику.

configurator

Класс, который отвечает за конфигурацию сервиса после его инстанцирования[4]. Как это выглядит на примере:

1
2
3
4
5
6
7
services:
  mymodule.configurator:
    class: Drupal\mymodule\MyModuleConfigurator
 
  mymodule.my_service:
    class: Drupal\mymodule\MyService\MyServiceClass
    configurator: ['@mymodule.configurator', configure]

При вызове сервиса mymodule.my_service, созданный экземпляр сервиса сначала будет обработан методом MyModuleConfigurator::configure().

factory

Класс фабрики, который отвечает за создание объекта сервиса[5].
Пример c новым синтаксисом:

1
2
3
4
services:
  mymodule.my_service:
    class: Drupal\mymodule\MyService\MyServiceClass
    factory:  Drupal\mymodule\MyService\MyServiceFactoryClass::get

Пример cо старым синтаксисом:

1
2
3
4
services:
  mymodule.my_service:
    class: Drupal\mymodule\MyService\MyServiceClass
    factory: ['Drupal\mymodule\MyService\MyServiceFactoryClass', get]

Свойства factory_class, factory_method, factory_service признаны устаревшими, начиная с версии Symfony 2.7[6].
Пример сервис-фабрики для создания объекта сервиса.

1
2
3
4
5
6
7
8
services:
  # объявляем фабрику как сервис.
  mymodule.my_service_factory:
    class: Drupal\mymodule\MyService\MyServiceFactoryClass
 
  mymodule.my_service:
    class: Drupal\mymodule\MyService\MyServiceClass
    factory: mymodule.my_service_factory:get

file

Файл, который будет включен до момента полной загрузки сервиса[7].
Пример:

1
2
3
4
services:
  mymodule.my_service:
    class: Drupal\mymodule\MyService\MyServiceClass
    file: path_to_file/myextrafile.php

Стоит отметить, что для подключения файла будет использоваться PHP функция require_once.

parent

Содержит имя абстрактного сервиса, чьи свойства будут наследоваться[8]. См. abstract

properties

Возможность установить public свойства для класса напрямую[9]. Пример, содержимое класса MyServiceClass

1
2
3
4
class MyServiceClass {
  public $object;
  // ...
}

В файле mymodule.services.yml

1
2
3
4
5
services:
  mymodule.my_service:
    class: Drupal\mymodule\MyService\MyServiceClass
    properties:
      object: '@mymodule.some_service'

Использование данного свойства в основном имеет одни недостатки:

  • невозможно контролировать момент когда зависимость установлена, т.е. она может быть изменена в любой момент времени жизни объекта.
  • невозможно использовать type hinting (контроль типов).

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

public

Определяет сервис как публичный или как приватный[10]. Приватные сервисы могут быть использованы только в качестве аргументов для других сервисов. Возможные значения:

  • true — сервис публичный (дефолтное значение).
  • false — сервис приватный.

scope

Определяет как долго экземпляр сервиса будет использоваться контейнером[11]. Возможные варианты:

  • container (значение по умолчанию) — один и тот же экземпляр используется каждый раз, когда вы запрашиваете его из этого контейнера.
  • prototype — новый экземпляр создается каждый раз, когда вы запрашиваете его из сервиса.
  • request — новый экземпляр создается для каждого подзапроса и недоступен вне запроса (например, в консоли).

synchronized

Свойство, указывающее на то, что сервис будет переконфигурирован при каждом изменении scope (свойство является устаревшим с версии symfony 2.7)[12]. Значения: true и false.

synthetic

Сервис может быть внедрен в контейнер вместо того, чтобы быть созданным контейнером[13]. Доступные значения:

  • true — synthetic сервис.
  • false — нормальный сервис (значение по умолчанию).

tags

Свойство, идентифицирующие группу сервисов[14]. Оно не вносит изменения в функциональность сервиса, но позволяют получить список всех services по определенному тэгу и использовать/модифицировать их специальным образом. Пример тегирования:

1
2
3
4
5
6
7
services:
  cache.toolbar:
    class: Drupal\Core\Cache\CacheBackendInterface
    tags:
      - { name: cache.bin }
    factory: cache_factory:get
    arguments: [toolbar]

Пример получения сервисов по тегу

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Adds cache_bins parameter to the container.
*/
class ListCacheBinsPass implements CompilerPassInterface {
 
 //...
 public function process(ContainerBuilder $container) {
   //...
   foreach ($container->findTaggedServiceIds('cache.bin') as $id => $attributes) {
     // выполнение некой логики...
   }
   //...
 }
 
}

Parameters файла services.yml

Parameters — это секция в файле *.services.yml, которая отвечает за установку параметров[15]. Это позволяет сконцентрировать “хардкодные” значения в одном месте (по аналогии с константами), а не искать их по всему файлу конфигурации сервисов.
Как правило секция parameters идет вначале файла *.services.yml. Пример:

1
2
parameters:
  mymodule.my_service_class: Drupal\mymodule\MyService\MyServiceClass

Для использования данного параметра в определении сервиса используем %.

1
2
3
services:
  mymodule.my_service:
    class: %mymodule.my_service_class%

Кроме того, можно задать массив параметров

1
2
parameters:
  mymodule.languages: [ru, en, by, de]

или таким образом

1
2
3
4
5
6
7
parameters:
  mymodule.category: 
    car:
      - audi
      - bmw
    moto:
      - suzuki

Альтер сервиса

Сервис можно заальтерить, т.е. изменить логику его работы[16]. Это может быть полезно, когда необходимо внести изменения в какой-либо сервис, не вмешиваясь в его код непосредственно. Для этого нужно реализовать собственный, унаследовав логику от класса ServiceProviderBase и указать метод alter().
Стоит обратить внимание на то, что данную фичу нужно использовать с осторожностью, т.к. сервис может альтериться многими модулями и это может спровоцировать проблемы, потому как спрогнозировать очередность выполнения альтера не представляется возможным.
Кроме того, для работы данной фичи в автоматическом режиме необходимо соблюдать требование по названию класса, т.е. имя класса должно быть MyModuleServiceProvider, имя модуля в формате CamelCase.
Пример:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace Drupal\mymodule;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
 
// ...
class MyModuleServiceProvider extends ServiceProviderBase {
 
  // ...
  public function alter(ContainerBuilder $container) {
    $definition = $container->getDefinition('language_manager');
    $definition->setClass('Drupal\mymodule\MyModuleLanguageManager');
  }
 
}

Создание кастомного сервиса

Итак, для того чтобы создать кастомный сервис, нужно выполнить несколько простых шагов:

  1. Создать файл MYMODULE.services.yml в корневой папке кастомного модуля.
  2. Описать в нем новый сервис.
    1
    2
    3
    
    services:
      mymodule.my_service:
        class: Drupal\mymodule\MyService\MyServiceClass
  3. Создать папку MyService и разместить ее в папке src/ модуля.
  4. Создать файл MyServiceClass.php примерно с таким содержимым:
    1
    2
    3
    4
    5
    6
    7
    8
    
    namespace Drupal\mymodule\MyService;
    // ...
    class MyServiceClass {
      // ...
      public function get() {
        // Описание некой логики и возвращение результата.
      }
    }

Чистим кеш. Далее, например, в контроллере какой-либо страницы создаем объект сервиса и вызываем метод get().

1
2
$service = \Drupal::service(‘mymodule.my_service’);
$data = $service->get();

Домашнее задание

Ответ по прошлому заданию

Для создания блока создадим необходимые директории — Plugin/Block. В папке Block создаем файл NewsBlock.php нашего класса, который будет отвечать за инициализацию будущего блока. Т.е. путь до файла с классом должен выглядеть так

1
news/src/Plugin/Block/NewsBlock.php

Структура класса NewsBlock приведена ниже

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
 
namespace Drupal\news\Plugin\Block;
 
use Drupal\Core\Block\BlockBase;
 
/**
* Class NewsBlock.
*
* @Block(
*   id = "newsblock",
*   admin_label = @Translation("News Block")
* )
*/
class NewsBlock extends BlockBase {
 
 /**
  * {@inheritdoc}
  */
 public function build() {
   return array('#markup' => $this->t('This is a news block'));
 }
 
}

Чистим кеш. Далее идем в админку Block Layout, используя меню Structure → Block Layout или урл admin/structure/block. Выбираем любой понравившийся регион (например, Sidebar first), жмем на кнопку Place block, далее находим в списке News Block и снова жмем на кнопку Place block.
блок news
Далее попадаем на страницу конфигурации (страницу в попапе). Здесь нас интересует вкладка Roles, на которой отмечаем, какой роли данный блок будет показан. По условию задания только авторизованным пользователям.
конфигурация блока news
Сохраняем изменения, перемещаем блок в верх списка для региона Sidebar First и снова сохраняем изменения.
блок news в сайдбаре
Итоговый результат — на главной странице видим созданный блок.
блок news на главной странице

Задание по текущему уроку

Написать сервис, который будет делать запрос на API стороннего сервиса и получать данные. Т.к. модуль news будет агрегировать новости, воспользуемся бесплатным сервисом https://newsapi.org/. Пусть для начала сервис возвращает список всех источников новостей (UI для этого списка пока не нужен). Можно использовать другой сторонний сервис, это не принципиально.

Дополнительная информация по статье

  1. https://www.drupal.org/docs/8/api/services-and-dependency-injection/structure-of-a-service-file - структура services.yml на d.org
  2. http://symfony.com/doc/current/service_container/alias_private.html#aliasing - алиасы из документации symfony.
  3. http://symfony.com/doc/current/service_container/injection_types.html#setter-injection - Setter Injection из документации symfony.
  4. https://symfony.com/doc/current/service_container/configurators.html#using-the-configurator - о свойстве configurator.
  5. http://symfony.com/doc/2.7/service_container/factories.html - использование factory.
  6. https://www.drupal.org/node/2489948 - устаревшее API Symfony с версии 2.7.
  7. http://symfony2-document.readthedocs.io/en/latest/book/service_container.html?highlight=services.yml%20#requiring-files - подключение файлов при создании сервиса из документации симфони.
  8. http://symfony.com/doc/current/service_container/parent_services.html - о свойстве parent.
  9. http://symfony.com/doc/current/service_container/injection_types.html#property-injection - о свойстве properties из документации symfony.
  10. http://symfony.com/doc/current/service_container/alias_private.html#marking-services-as-public-private - о приватных и публичных сервисах.
  11. https://symfony.com/doc/2.4/cookbook/service_container/scopes.html#understanding-scopes - о scopes из документации симфони.
  12. https://symfony.com/doc/2.4/cookbook/service_container/scopes.html#using-synchronized-service - о свойстве synchronized.
  13. https://symfony.com/doc/current/service_container/synthetic_services.html#main - synthetic из документации symfony.
  14. http://symfony.com/doc/current/service_container/tags.html - тегирование сервисов.
  15. http://symfony.com/doc/current/service_container/parameters.html - parameters из документации симфони.
  16. https://www.drupal.org/docs/8/api/services-and-dependency-injection/altering-existing-services-providing-dynamic-services - alter сервиса из документации d.org.
  17. Версии программных продуктов, используемых в статье: Drupal 8.2.6

3 Комментария

Аватар пользователя Гость

В коде создания объекта

В коде создания объекта сервиса вместо:

1
$service = \Drupal::get('mymodule.my_service');

Должно быть:

1
$service = \Drupal::service('mymodule.my_service');

Спасибо за уроки. Жду продолжения.