Урок 5. Сервисы
Опубликовано вт, 07/03/2017 - 01:55
Сервисы — это объекты, которыми управляет сервис контейнер (он же контейнер зависимостей, Dependency injection). По своей сути сервисы как и плагины, представляют собой кусочек функциональности, который выполняет конкретную задачу. Например, отправка писем, получение данных от какого-либо стороннего API, подключение к базе данных, работа с кешем и т.д.
Сервисы определяются в специальном файле *.services.yml. Данный файл в своем имени использует префикс модуля, в котором он определен.
Содержание
- Структура файла services.yml
- abstract
- alias
- arguments
- calls
- class
- configurator
- factory
- file
- parent
- properties
- public
- scope
- synchronized
- synthetic
- tags
- Parameters файла 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'); } } |
Создание кастомного сервиса
Итак, для того чтобы создать кастомный сервис, нужно выполнить несколько простых шагов:
- Создать файл MYMODULE.services.yml в корневой папке кастомного модуля.
- Описать в нем новый сервис.
1 2 3
services: mymodule.my_service: class: Drupal\mymodule\MyService\MyServiceClass
- Создать папку MyService и разместить ее в папке src/ модуля.
- Создать файл 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.
Далее попадаем на страницу конфигурации (страницу в попапе). Здесь нас интересует вкладка Roles, на которой отмечаем, какой роли данный блок будет показан. По условию задания только авторизованным пользователям.
Сохраняем изменения, перемещаем блок в верх списка для региона Sidebar First и снова сохраняем изменения.
Итоговый результат — на главной странице видим созданный блок.
Задание по текущему уроку
Написать сервис, который будет делать запрос на API стороннего сервиса и получать данные. Т.к. модуль news будет агрегировать новости, воспользуемся бесплатным сервисом https://newsapi.org/. Пусть для начала сервис возвращает список всех источников новостей (UI для этого списка пока не нужен). Можно использовать другой сторонний сервис, это не принципиально.
Дополнительная информация по статье
- https://www.drupal.org/docs/8/api/services-and-dependency-injection/structure-of-a-service-file - структура services.yml на d.org
- http://symfony.com/doc/current/service_container/alias_private.html#aliasing - алиасы из документации symfony.
- http://symfony.com/doc/current/service_container/injection_types.html#setter-injection - Setter Injection из документации symfony.
- https://symfony.com/doc/current/service_container/configurators.html#using-the-configurator - о свойстве configurator.
- http://symfony.com/doc/2.7/service_container/factories.html - использование factory.
- https://www.drupal.org/node/2489948 - устаревшее API Symfony с версии 2.7.
- http://symfony2-document.readthedocs.io/en/latest/book/service_container.html?highlight=services.yml%20#requiring-files - подключение файлов при создании сервиса из документации симфони.
- http://symfony.com/doc/current/service_container/parent_services.html - о свойстве parent.
- http://symfony.com/doc/current/service_container/injection_types.html#property-injection - о свойстве properties из документации symfony.
- http://symfony.com/doc/current/service_container/alias_private.html#marking-services-as-public-private - о приватных и публичных сервисах.
- https://symfony.com/doc/2.4/cookbook/service_container/scopes.html#understanding-scopes - о scopes из документации симфони.
- https://symfony.com/doc/2.4/cookbook/service_container/scopes.html#using-synchronized-service - о свойстве synchronized.
- https://symfony.com/doc/current/service_container/synthetic_services.html#main - synthetic из документации symfony.
- http://symfony.com/doc/current/service_container/tags.html - тегирование сервисов.
- http://symfony.com/doc/current/service_container/parameters.html - parameters из документации симфони.
- https://www.drupal.org/docs/8/api/services-and-dependency-injection/altering-existing-services-providing-dynamic-services - alter сервиса из документации d.org.
- Версии программных продуктов, используемых в статье: Drupal 8.2.6
3 Комментария
Гость - чт, 09/03/2017 - 00:36
В коде создания объекта
В коде создания объекта сервиса вместо:
Должно быть:
Спасибо за уроки. Жду продолжения.
nightdevel - чт, 09/03/2017 - 01:16
Спасибо за замечание.
Спасибо за замечание. Поправил
гость - пн, 24/04/2017 - 14:42
Эта серия статей по API
Эта серия статей по API Drupal 8 обещает быть лучшей, что есть в рунете! Спасибо автору!