Локаторы сервисов

Иногда сервису требуется получить доступ к некоторым другим сервисам, даже не имея уверенности в том, что все из них действительно будут использованы. В таких случаях, вы можете захотеть, чтобы инстанциирование сервисов было ленивым. Однако, это невозможно при использовании ясного внедрения зависимости, так как сервисы не предназначены для того, чтобы быть ленивыми (lazy) (см. Lazy Services).

Реальным примером являются приложения, которые реализуют Команду (шаблон проектирования), используя 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, чтобы превратить его в локатор сервисов:

  • YAML
    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'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
    
            <service id="app.command_handler_locator" class="Symfony\Component\DependencyInjection\ServiceLocator">
                <argument type="collection">
                    <argument key="AppBundle\FooCommand" type="service" id="app.command_handler.foo" />
                    <argument key="AppBundle\BarCommand" type="service" id="app.command_handler.bar" />
                </argument>
                <tag name="container.service_locator" />
            </service>
    
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    use Symfony\Component\DependencyInjection\ServiceLocator;
    use Symfony\Component\DependencyInjection\Reference;
    
    //...
    
    $container
        ->register('app.command_handler_locator', ServiceLocator::class)
        ->addTag('container.service_locator')
        ->setArguments(array(array(
            'AppBundle\FooCommand' => new Reference('app.command_handler.foo'),
            'AppBundle\BarCommand' => new Reference('app.command_handler.bar'),
        )))
    ;
    

Note

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

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

  • YAML
    1
    2
    3
    4
    services:
    
        AppBundle\CommandBus:
            arguments: ['@app.command_handler_locator']
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
    
            <service id="AppBundle\CommandBus">
                <argument type="service" id="app.command_handler.locator" />
            </service>
    
        </services>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    8
    9
    use AppBundle\CommandBus;
    use Symfony\Component\DependencyInjection\Reference;
    
    //...
    
    $container
        ->register(CommandBus::class)
        ->setArguments(array(new Reference('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);

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.