Підписники та локатори сервісів
Дата оновлення перекладу 2022-12-23
Підписники та локатори сервісів
Іноді сервісу необхідний доступ до декількох інших сервісів, не маючи гарантій того,
що вони дійсно будуть використані. У таких випадках, вам може захотітися лінивого
запуску сервісів. Однак це неможливо з використанням чіткого впровадження залежності,
так як сервіси взагалі не повинні бути lazy
(див. Ліниві сервіси).
Це може бути типовим для вашого контролера, де ви можете захотіти впровадити декілька сервісів у конструктор, але виклична дія використовує лише деякі з них. Інший приклад - додатки, що реалізують шаблон Команди, використовуючи CommandBus для відображення обробників команд за іменами класів команд, та їх використання для оброрбки відповідної команди, коли вона буде запитана:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
// src/CommandBus.php
namespace App;
// ...
class CommandBus
{
/**
* @var CommandHandler[]
*/
private $handlerMap;
public function __construct(array $handlerMap)
{
$this->handlerMap = $handlerMap;
}
public function handle(Command $command)
{
$commandClass = get_class($command);
if (!isset($this->handlerMap[$commandClass])) {
return;
}
return $this->handlerMap[$commandClass]->handle($command);
}
}
// ...
$commandBus->handle(new FooCommand());
Враховуючи, що в один момент часу обробляється лише одна команда, запуск усіх інших обрробників команд недоречний. Можливим рішенням лінивого завантаження обробників буде їх впровадження у головний контейнер впровадження залежностей.
Однак, впровадження контейнера цілком не рекомендовано, так як це надає занадто широкий доступ до існуючих сервісів і приховує реальні залежності сервісів. Також це вимагає того, щоб сервіси були публічними, що за замовчуванням не так у додатках Symfony.
Підписники сервісів призначені для того, щоб вирішувати цю проблему, надаючи доступ до набору передвизначених сервісів, запускаючи їх лише тоді, коли потрібно через Локатор сервісів - окремий ліниво завантажуваний контейнер.
Визначення підписника подій
Спочатку, перетворіть CommandBus
на реалізацію ServiceSubscriberInterface.
Використайте його метод getSubscribedServices()
, щоб додати стільки сервісів, скільки
необхідно, у підписник подій і змініть підказку контейнера на PSR-11 ContainerInterface
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
// src/CommandBus.php
namespace App;
use App\CommandHandler\BarHandler;
use App\CommandHandler\FooHandler;
use Psr\Container\ContainerInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
class CommandBus implements ServiceSubscriberInterface
{
private $locator;
public function __construct(ContainerInterface $locator)
{
$this->locator = $locator;
}
public static function getSubscribedServices(): array
{
return [
'App\FooCommand' => FooHandler::class,
'App\BarCommand' => BarHandler::class,
];
}
public function handle(Command $command)
{
$commandClass = get_class($command);
if ($this->locator->has($commandClass)) {
$handler = $this->locator->get($commandClass);
return $handler->handle($command);
}
}
}
Tip
Якщо контейнер не містить підписані сервіси, перевірте, щоб у вас була підключена
автоконфігурація . Ви також можете вручну додати тег
container.service_subscriber
.
Впроваджений сервіс є екземпляром ServiceLocator,
який реалізує PSR-11 ContainerInterface
, але також є викличним:
1 2 3 4
// ...
$handler = ($this->locator)($commandClass);
return $handler->handle($command);
Додавання сервісів
Для того, щоб додати нову залежність у підписник подій, використайте метод
getSubscribedServices()
, щоб додавати типи сервісів для їх включення у
локатор сервісів:
1 2 3 4 5 6 7 8 9
use Psr\Log\LoggerInterface;
public static function getSubscribedServices(): array
{
return [
// ...
LoggerInterface::class,
];
}
Типи сервісів також можуть мати імʼя сервісу для внутрішнього використання:
1 2 3 4 5 6 7 8 9
use Psr\Log\LoggerInterface;
public static function getSubscribedServices(): array
{
return [
// ...
'logger' => LoggerInterface::class,
];
}
При розширенні класу, який також реалізує ServiceSubscriberInterface
, ваша
відповідальність - викликати батька пи перевизначенні методу. Це зазвичай відбувається
при розширенні AbstractController
:
1 2 3 4 5 6 7 8 9 10 11 12 13
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class MyController extends AbstractController
{
public static function getSubscribedServices(): array
{
return array_merge(parent::getSubscribedServices(), [
// ...
'logger' => LoggerInterface::class,
]);
}
}
Опціональні сервіси
Для додаткових залежностей, додайте на початку типу сервісу ?
, щоб уникнути
помилок, якщо відповідний сервіс не буде знайдено у сервіс-контейнері:
1 2 3 4 5 6 7 8 9
use Psr\Log\LoggerInterface;
public static function getSubscribedServices(): array
{
return [
// ...
'?'.LoggerInterface::class,
];
}
Note
Переконайтеся, що додатковий сервіс існує, викликавши has()
у локаторі
сервісу до виклику самого сервісу.
Cервіси з псевдонімами
За замовчуванням, для співставлення типу сервісу з сервісом з сервіс-контейнера,
використовується автомонтування. Якщо ви не використовуєте автомонтування, або вам
потрібно додати нетрадиційний сервіс в якості залежності, використайте тег
container.service_subscriber
, щоб провести тип сервісу до сервісу.
- YAML
- XML
- PHP
1 2 3 4 5
# config/services.yaml
services:
App\CommandBus:
tags:
- { name: 'container.service_subscriber', key: 'logger', id: 'monolog.logger.event' }
Tip
Атрибут key
може бути пропущено, якщо внутрішнє імʼя сервісу співпадає з
іменем у сервіс-контейнері.
Додавання атрибутів впровадження залежності
6.2
Можливість додавати атрибути була представлена в Symfony 6.2.
Альтернативою псевдонімам сервісів у вашій конфігурації є конфігурація наступних
атрибутів впровадження залежності у методі getSubscribedServices()
напряму:
Це робиться шляхом повернення з getSubscribedServices()
масиву обʼєктів
SubscribedService (які можуть
бути скомбіновані зі стандартними значеннями string[]
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Contracts\Service\Attribute\SubscribedService;
public static function getSubscribedServices(): array
{
return [
// ...
new SubscribedService('logger', LoggerInterface::class, attributes: new Autowire(service: 'monolog.logger.event')),
// чи може подія використовувати параметри
new SubscribedService('env', string, attributes: new Autowire('%kernel.environment%')),
// Target
new SubscribedService('event.logger', LoggerInterface::class, attributes: new Target('eventLogger')),
// TaggedIterator
new SubscribedService('loggers', 'iterable', attributes: new TaggedIterator('logger.tag')),
// TaggedLocator
new SubscribedService('handlers', ContainerInterface::class, attributes: new TaggedLocator('handler.tag')),
];
}
Note
Приклад вище вимагає використання версії symfony/service-contracts
3.2
або вище.
Визначення локатора сервісів
Щоб вручну визначити локатор сервісів і впровадити його в інший сервіс, створіть
аргумент типу service_locator
:
- YAML
- XML
- PHP
1 2 3 4 5 6
# config/services.yaml
services:
App\CommandBus:
arguments: !service_locator
App\FooCommand: '@app.command_handler.foo'
App\BarCommand: '@app.command_handler.bar'
Як продемонстровано у попередніх розділах, конструктор класу CommandBus
повинен додати
підказку до свого аргументу за допомогою ContainerInterface
. Потім, ви можете отримати
будь-який з сервісів локатора сервісів через його ID (наприклад, $this->locator->get('App\FooCommand')
).
Повторне використання локатора сервісів у декількох сервісах
Якщо ви впроваджуєте один і той же локатор сервісів у декілька сервісів, краще визначати
локатор сервісів як окремий сервіс, а потім впроваджувати його в інші сервіси. Щоб зробити
це, створіть нове визначення сервісу, використовуючи клас ServiceLocator
:
- YAML
- XML
- PHP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
# config/services.yaml
services:
app.command_handler_locator:
class: Symfony\Component\DependencyInjection\ServiceLocator
arguments:
-
App\FooCommand: '@app.command_handler.foo'
App\BarCommand: '@app.command_handler.bar'
# якщо ви не використовуєте автоконфігурацію сервісу за замовчуванням,
# додайте наступний тег до визначення сервісу:
# tags: ['container.service_locator']
# якщо елемент не має ключа, використовується ID оригінального сервісу
app.another_command_handler_locator:
class: Symfony\Component\DependencyInjection\ServiceLocator
arguments:
-
- '@app.command_handler.baz'
Note
Сервіси, визначені в аргументі локатора сервісів, повинні включати в себе ключі, які пізніше стають їх унікальними ідентифікаторами всередині локатора.
Тепер ви можете впровадити локатор сервісів у будь-які інші сервіси:
- YAML
- XML
- PHP
1 2 3 4
# config/services.yaml
services:
App\CommandBus:
arguments: ['@app.command_handler_locator']
Використання локаторів сервісів у пропусках компілятора
У пропусках компілятора рекомендовано використовувати метод register() для створення локаторів сервісів. Це створить вам деякий шаблон і матиме ідентичні локатори серед всіх сервісів, що посилаються на них:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
public function process(ContainerBuilder $container): void
{
// ...
$locateableServices = [
// ...
'logger' => new Reference('logger'),
];
$myService->addArgument(ServiceLocatorTagPass::register($container, $locateableServices));
}
Індексування колекції сервісів
Сервіси, що передаються локатору сервісів, можуть визначати власний індекс,
використовуючи довільний атрибут, імʼя якого визначається у сервіс-контейнері
як index_by
.
У наступному прикладі, локатор App\Handler\HandlerCollection
отримує всі сервіси
з тегом app.handler
і вони індексуються, використовуючи значення атрибуту тегу
key
(як визначено в опції локатора index_by
):
- YAML
- XML
- PHP
1 2 3 4 5 6 7 8 9 10 11 12 13
# config/services.yaml
services:
App\Handler\One:
tags:
- { name: 'app.handler', key: 'handler_one' }
App\Handler\Two:
tags:
- { name: 'app.handler', key: 'handler_two' }
App\Handler\HandlerCollection:
# впровадити всі сервіси з тегом app.handler в якості першого аргументу
arguments: [!tagged_locator { tag: 'app.handler', index_by: 'key' }]
Всередині цього локатора, ви можете вилучити сервіси за індексом, використовуючи
значення атрибуту key
. Наприклад, щоб отримати сервіс App\Handler\Two
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// src/Handler/HandlerCollection.php
namespace App\Handler;
use Symfony\Component\DependencyInjection\ServiceLocator;
class HandlerCollection
{
public function __construct(ServiceLocator $locator)
{
$handlerTwo = $locator->get('handler_two');
}
// ...
}
Замість визначення індексу у визначенні сервісу, ви можете повернути його значення
в методі під назвою getDefaultIndexName()
всередині класу, асоційованого з сервісом:
1 2 3 4 5 6 7 8 9 10 11 12
// src/Handler/One.php
namespace App\Handler;
class One
{
public static function getDefaultIndexName(): string
{
return 'handler_one';
}
// ...
}
Якщо ви хочете використати інше імʼя методу, додайте атрибут default_index_method
до локатору сервісів, визначаючи імʼя його користувацього методу:
- YAML
- XML
- PHP
1 2 3 4 5 6
# config/services.yaml
services:
# ...
App\HandlerCollection:
arguments: [!tagged_locator { tag: 'app.handler', index_by: 'key', default_index_method: 'myOwnMethodName' }]
Note
Так як код не повинен відповідати за визначення того, як будуть використані локатори,
ключ конфігурації (key
- у прикладі вище) повинен бути встановлений так, щоб
користувацький метод міг викликатися в якості резервного.
Риса підписників сервісів
ServiceSubscriberTrait надає реалізацію
для ServiceSubscriberInterface, яка переглядає
всі методи у вашому класі, марковані атрибутом
SubscribedService. Він надає ServiceLocator
для сервісів кожного типу зворотного значення методу. Id сервісу - __METHOD__
. Це
дозволяє вам додавати залежності до ваших сервісів, засновуючись на підказках методів
помічників:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
// src/Service/MyService.php
namespace App\Service;
use Psr\Log\LoggerInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\Service\Attribute\SubscribedService;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Symfony\Contracts\Service\ServiceSubscriberTrait;
class MyService implements ServiceSubscriberInterface
{
use ServiceSubscriberTrait;
public function doSomething()
{
// $this->router() ...
// $this->logger() ...
}
#[SubscribedService]
private function router(): RouterInterface
{
return $this->container->get(__METHOD__);
}
#[SubscribedService]
private function logger(): LoggerInterface
{
return $this->container->get(__METHOD__);
}
}
Це дозволяє вам створювати риси помічників, на кшталт RouterAware, LoggerAware та ін... та компілювати з їх допомогою ваші сервіси:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
// src/Service/LoggerAware.php
namespace App\Service;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\Service\Attribute\SubscribedService;
trait LoggerAware
{
#[SubscribedService]
private function logger(): LoggerInterface
{
return $this->container->get(__CLASS__.'::'.__FUNCTION__);
}
}
// src/Service/RouterAware.php
namespace App\Service;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\Service\Attribute\SubscribedService;
trait RouterAware
{
#[SubscribedService]
private function router(): RouterInterface
{
return $this->container->get(__CLASS__.'::'.__FUNCTION__);
}
}
// src/Service/MyService.php
namespace App\Service;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Symfony\Contracts\Service\ServiceSubscriberTrait;
class MyService implements ServiceSubscriberInterface
{
use ServiceSubscriberTrait, LoggerAware, RouterAware;
public function doSomething()
{
// $this->router() ...
// $this->logger() ...
}
}
Caution
При створенні цих рис помічників, id сервісу не може бути __METHOD__
, так
як воно включатиме в себе імʼя риси, а не класу. Замість цьього, використайте
в якості id сервісу __CLASS__.'::'.__FUNCTION__
.
Атрибути SubscribedService
6.2
Можливість додавати атрибути була представлена в Symfony 6.2.
Ви можете використати аргумент attributes
SubscribedService
, щоб додати
будь-які з наступних атрибутів впровадження залежності:
Ось приклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
// src/Service/MyService.php
namespace App\Service;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\Service\Attribute\SubscribedService;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Symfony\Contracts\Service\ServiceSubscriberTrait;
class MyService implements ServiceSubscriberInterface
{
use ServiceSubscriberTrait;
public function doSomething()
{
// $this->environment() ...
// $this->router() ...
// $this->logger() ...
}
#[SubscribedService(attributes: new Autowire('%kernel.environment%'))]
private function environment(): string
{
return $this->container->get(__METHOD__);
}
#[SubscribedService(attributes: new Autowire(service: 'router'))]
private function router(): RouterInterface
{
return $this->container->get(__METHOD__);
}
#[SubscribedService(attributes: new Target('requestLogger'))]
private function logger(): LoggerInterface
{
return $this->container->get(__METHOD__);
}
}
Note
Приклад вище вимагає використання версії symfony/service-contracts
3.2
або вище.
Тестування підписника сервісу
Щоб модулььно тестувати підписника сервісу, ви можете створити підробний
ServiceLocator
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
use Symfony\Component\DependencyInjection\ServiceLocator;
$container = new class() extends ServiceLocator {
private $services = [];
public function __construct()
{
parent::__construct([
'foo' => function () {
return $this->services['foo'] = $this->services['foo'] ?? new stdClass();
},
'bar' => function () {
return $this->services['bar'] = $this->services['bar'] ?? $this->createBar();
},
]);
}
private function createBar()
{
$bar = new stdClass();
$bar->foo = $this->get('foo');
return $bar;
}
};
$serviceSubscriber = new MyService($container);
// ...
Ще однією альтернативою є імітація з використанням PHPUnit
:
1 2 3 4 5 6 7 8 9 10 11 12 13
use Psr\Container\ContainerInterface;
$container = $this->createMock(ContainerInterface::class);
$container->expects(self::any())
->method('get')
->willReturnMap([
['foo', $this->createStub(Foo::class)],
['bar', $this->createStub(Bar::class)],
])
;
$serviceSubscriber = new MyService($container);
// ...