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

Дата оновлення перекладу 2023-06-05

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

Іноді сервісу потрібно отримати доступ до деяких інших сервісів, навіть не маючи впевненості в тому, що всі з них дійсно будуть використані. У таких випадках, ви можете захотіти, щоб інстанціювання сервісів було ледачим. Однак, це неможливо при використанні ясного впровадження залежності, оскільки сервіси не призначені для того, щоб бути ледачими (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);