Дата обновления перевода 2022-02-01

Подписчики и локаторы сервисов

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

Это может быть типичным для ваших контроллеров, где вы можете захотеть внедрить несколько сервисов в конструктор, но вызываемое действие использует только некоторые из них. Другой пример - приложения, реализующие шаблон Команды, используя CommandBus для отображения обработчиков команд по именам классов Команд, и их использования для обработки соответствующей команды, когда она будет запрошена:

// 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:

// 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, но также является вызываемым:

// ...
$handler = ($this->locator)($commandClass);

return $handler->handle($command);

Добавление сервисов

Для того, чтобы добавить новую зависимость в подписчик событий, используйте метод getSubscribedServices(), чтобы добавлять типы сервисов для включения их в локатор сервисов:

use Psr\Log\LoggerInterface;

public static function getSubscribedServices(): array
{
    return [
        // ...
        LoggerInterface::class,
    ];
}

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

use Psr\Log\LoggerInterface;

public static function getSubscribedServices(): array
{
    return [
        // ...
        'logger' => LoggerInterface::class,
    ];
}

При расширении класса, который также реализует ServiceSubscriberInterface, ваша ответственность - вызвать родителя при переопределении метода. Это обычно происходит при расширении AbstractController:

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,
        ]);
    }
}

Дополнительные сервисы

Для дополнительных зависимостей, добавьте к началу типу сервиса ?, чтобы избежать ошибок, если соответствующий сервис не будет найден в сервис-контейнере:

use Psr\Log\LoggerInterface;

public static function getSubscribedServices(): array
{
    return [
        // ...
        '?'.LoggerInterface::class,
    ];
}

Note

Убедитесь, что дополнительный сервис существует, вызвав has() в локаторе сервиса до вызова самого сервиса.

Cервисы с псевдонимами

По умолчанию, для сопоставления типа сервиса с сервисом из сервис-контейнера используется автомонтирование. Если вы не используете автомонтирование, или вам нужно добавить нетрадиционный сервис в качестве зависимости, используйте тег container.service_subscriber, чтобы провести тип сервиса к сервису.

  • YAML
    1
    2
    3
    4
    5
    # config/services.yaml
    services:
        App\CommandBus:
            tags:
                - { name: 'container.service_subscriber', key: 'logger', id: 'monolog.logger.event' }
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    <!-- config/services.xml -->
    <?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 https://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
    
            <service id="App\CommandBus">
                <tag name="container.service_subscriber" key="logger" id="monolog.logger.event"/>
            </service>
    
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    use App\CommandBus;
    
    return function(ContainerConfigurator $configurator) {
        $services = $configurator->services();
    
        $services->set(CommandBus::class)
            ->tag('container.service_subscriber', ['key' => 'logger', 'id' => 'monolog.logger.event']);
    };
    

Tip

Атрибут key может быть опущен, если внутренне имя сервиса совпадает с именем в сервис-контейнере.

Определение локатора сервисов

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

  • YAML
    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'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <!-- config/services.xml -->
    <?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 https://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="App\CommandBus">
                <argument type="service_locator">
                    <argument key="App\FooCommand" type="service" id="sapp.command_handler.foo"/>
                    <argument key="App\BarCommandr" type="service" id="app.command_handler.bar"/>
                    <!-- if the element has no key, the ID of the original service is used -->
                    <argument type="service" id="app.command_handler.baz"/>
                </argument>
            </service>
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    use App\CommandBus;
    
    return function(ContainerConfigurator $configurator) {
        $services = $configurator->services();
    
        $services->set(CommandBus::class)
            ->args([service_locator([
                'App\FooCommand' => ref('app.command_handler.foo'),
                'App\BarCommand' => ref('app.command_handler.bar'),
                // if the element has no key, the ID of the original service is used
                ref('app.command_handler.baz'),
            ])]);
    };
    

Как показано в предыдущих разделах, конструктор класса CommandBus должен типизировать свой аргумент с помощью ContainerInterface. Затем, вы можете получить любой из сервисов локатора сервисов через его ID (например, $this->locator->get('App\FooCommand')).

Повторное использование локатора сервисов в нескольких сервисах

Если вы внедряете один и тот же локатор сервисов в несколько сервисов, лучше определять локатор сервисов как отдельный сервис, а затем внедрять его в другие сервисы. Чтобы сделать это, создайте новое определение сервиса, используя класс ServiceLocator:

  • YAML
     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'
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <!-- config/services.xml -->
    <?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 https://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="App\FooCommand" type="service" id="app.command_handler.foo"/>
                    <argument key="App\BarCommand" type="service" id="app.command_handler.bar"/>
                    <!-- если элемент не имеет ключа, используется ID изначального сервиса -->
                    <argument type="service" id="app.command_handler.baz"/>
                </argument>
                <!--
                    если вы не используете автоконфигурацию сервиса по умолчанию,
                    добавьте следующий тег к определению сервиса:
                    <tag name="container.service_locator"/>
                -->
            </service>
    
        </services>
    </container>
    
  • PHP
     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
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    use Symfony\Component\DependencyInjection\ServiceLocator;
    
    return function(ContainerConfigurator $configurator) {
        $services = $configurator->services();
    
        $services->set('app.command_handler_locator', ServiceLocator::class)
            // В версиях, предшествующих Symfony 5.1, функция service() называлась ref()
            ->args([[
                'App\FooCommand' => service('app.command_handler.foo'),
                'App\BarCommand' => service('app.command_handler.bar'),
            ]])
            // если вы не используете автоконфигурацию сервиса по умолчанию,
            // добавьте следующий тег к определению сервиса:
            // ->tag('container.service_locator')
        ;
    
        // если элемент не имеет ключа, используется ID изначального сервиса
        $services->set('app.another_command_handler_locator', ServiceLocator::class)
            ->args([[
                service('app.command_handler.baz'),
            ]])
        ;
    };
    

Note

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

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

  • YAML
    1
    2
    3
    4
    # config/services.yaml
    services:
        App\CommandBus:
            arguments: ['@app.command_handler_locator']
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    <!-- config/services.xml -->
    <?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 https://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
    
            <service id="App\CommandBus">
                <argument type="service" id="app.command_handler_locator"/>
            </service>
    
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    use App\CommandBus;
    
    return function(ContainerConfigurator $configurator) {
        $services = $configurator->services();
    
        $services->set(CommandBus::class)
            ->args([service('app.command_handler_locator')]);
    };
    

Использование локаторов сервисов в пропусках компилятора

В пропусках компилятора рекомендуется использовать метод register() для создания локаторов сервисов. Это создаст вам некий шаблон, и будет иметь идентичные локаторы среди всех сервисов, ссылающихся на них:

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
     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' }]
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <!-- config/services.xml -->
    <?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
            https://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="App\Handler\One">
                <tag name="app.handler" key="handler_one"/>
            </service>
    
            <service id="App\Handler\Two">
                <tag name="app.handler" key="handler_two"/>
            </service>
    
            <service id="App\HandlerCollection">
                <!-- внедрить все сервисы с тегом app.handler в качестве первого аргумента -->
                <argument type="tagged_locator" tag="app.handler" index-by="key"/>
            </service>
        </services>
    </container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    return function(ContainerConfigurator $configurator) {
        $services = $configurator->services();
    
        $services->set(App\Handler\One::class)
            ->tag('app.handler', ['key' => 'handler_one'])
        ;
    
        $services->set(App\Handler\Two::class)
            ->tag('app.handler', ['key' => 'handler_two'])
        ;
    
        $services->set(App\Handler\HandlerCollection::class)
            // внедрить все сервисы с тегом app.handler в качестве первого аргумента
            ->args([tagged_locator('app.handler', 'key')])
        ;
    };
    

Внутри этого локатора, вы можете извлечь сервисы по индексу, используя значение атрибута key. Например, чтобы получить сервис App\Handler\Two:

// 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() внутри класса, ассоциированного с сервисом:

// src/Handler/One.php
namespace App\Handler;

class One
{
    public static function getDefaultIndexName(): string
    {
        return 'handler_one';
    }

    // ...
}

Если вы хотите использоваь другое имя метода, добавьте атрибут default_index_method к локатору сервисов, определяя имя его пользовательского метода:

  • YAML
    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' }]
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    <!-- config/services.xml -->
    <?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
            https://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
    
            <!-- ... -->
    
            <service id="App\HandlerCollection">
                <argument type="tagged_locator" tag="app.handler" index-by="key" default-index-method="myOwnMethodName"/>
            </service>
        </services>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // config/services.php
    namespace Symfony\Component\DependencyInjection\Loader\Configurator;
    
    return function(ContainerConfigurator $configurator) {
        $configurator->services()
            ->set(App\HandlerCollection::class)
                ->args([tagged_locator('app.handler', 'key', 'myOwnMethodName')])
        ;
    };
    

Note

Так как код не должен отвечать за определение того, как будут использованы локаторы, ключ конфигурации (key - в примере выше) должен быть установлен так, чтобы пользовательский метод мог вызываться в качестве резервного.

Черта подписчика сервисов

ServiceSubscriberTrait предоставляет реализацию для ServiceSubscriberInterface, которая просматривает все методы в вашем классе, маркированные атрибутом SubscribedService. Он предоставляет ServiceLocator для сервисов каждого типа возвращаемого значения метода. Id сервиса - __METHOD__. Это позволяет вам добавлять зависимости к вашим сервисам, основываясь на подсказах методов помощников:

// 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, и др… и компилировать с их помощью ваши сервисы:

// 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__.

Deprecated since version 5.4: Определение ваших методов подписанного сервиса с помощью атрибута SubscribedService, было добавлено в Symfony 5.4. Ранее, любые методы без аргументов и типа возвращамого значения, были подписаны. Это все еще работает в версии 5.4, но устарело (только при использовании PHP 8), и будет удалено в версии 6.0.

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