Локаторы сервисов
Дата обновления перевода 2023-07-14
Локаторы сервисов
Иногда сервису требуется получить доступ к некоторым другим сервисам, даже не
имея уверенности в том, что все из них действительно будут использованы. В таких
случаях, вы можете захотеть, чтобы инстанциирование сервисов было ленивым. Однако,
это невозможно при использовании ясного внедрения зависимости, так как сервисы не
предназначены для того, чтобы быть ленивыми (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
// ...
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());
Учитывая, что одновременно обрабатывается только одна команда, инстанциирование всех других обработчиков команд необязательно. Возможным решением для ленивой загрузки обработчиков может стать внедрение всего контейнера внедрения зависимостей:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
use Symfony\Component\DependencyInjection\ContainerInterface;
class CommandBus
{
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function handle(Command $command)
{
$commandClass = get_class($command);
if ($this->container->has($commandClass)) {
$handler = $this->container->get($commandClass);
return $handler->handle($command);
}
}
}
Однако, внедрение всего контейнера не поощряется, так как это предоставляет слишком широкий доступ к существующим сервисам и скрывает настоящие зависимости сервисов.
Локаторы сервисов предназначены для решения этой проблемы, путём предоставления доступа к набору предопределённых сервисов, инстанциируя их только тогда, когда они действительно необходимы.
Определение локатора сервиса
Для начала, определите новый сервис для локатора сервиса. Используйте его
опцию arguments
, чтобы включить в него такое количество сервисов, которое
необходимо, и добавьте тег container.service_locator
, чтобы превратить
его в локатор сервисов:
1 2 3 4 5 6 7 8 9
services:
app.command_handler_locator:
class: Symfony\Component\DependencyInjection\ServiceLocator
tags: ['container.service_locator']
arguments:
-
AppBundle\FooCommand: '@app.command_handler.foo'
AppBundle\BarCommand: '@app.command_handler.bar'
Note
Сервисы, определённые в аргументе локатора сервисов должны иметь ключи, которые позже становятся уникальными идентификаторами внутри локатора.
Теперь вы можете использовать локатор сервисов, внедряя его в любой другой сервис:
1 2 3 4
services:
AppBundle\CommandBus:
arguments: ['@app.command_handler_locator']
Tip
Если локатор сервисов не предназначен для использования многими сервисами, то лучше создать и внедрить его в качестве анонимного сервиса.
Использование
Вернёмся к предыдущему примеру с 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
// ...
use Psr\Container\ContainerInterface;
class CommandBus
{
/**
* @var ContainerInterface
*/
private $handlerLocator;
// ...
public function handle(Command $command)
{
$commandClass = get_class($command);
if (!$this->handlerLocator->has($commandClass)) {
return;
}
$handler = $this->handlerLocator->get($commandClass);
return $handler->handle($command);
}
}
Внедрённый сервис - экземпляр ServiceLocator,
который реализует PSR-11 ContainerInterface
, но также является вызваемым:
1 2 3 4 5
// ...
$locateHandler = $this->handlerLocator;
$handler = $locateHandler($commandClass);
return $handler->handle($command);