Дата обновления перевода 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.