Как работать с тегами сервисов
Дата обновления перевода 2023-07-24
Как работать с тегами сервисов
Теги сервисов - это способ сообщить Symfony или другим сторонним пакетам, что ваш сервис должен быть зарегистрирован каким-то особым образом. Возьмите следующий пример:
1 2 3 4
# config/services.yaml
services:
App\Twig\AppExtension:
tags: ['twig.extension']
Сервисы, с тегом twig.extension
собираются во время инициализации
TwigBundle и добавляются в Twig как расширения.
Другие теги используются для интеграции ваших сервисов в другие системы. Чтобы
увидеть все доступные теги в базовом фреймворке Symfony, посмотрите
Встроенные сервис-теги Symfony. Каждый из них имеет разные эффект на ваш сервис, и
многие теги требуют дополнительных аргументов (кроме параметра name
).
Для большинства пользователей - это всё, что вам нужно знать. Если вы хотите углубиться в изучение того, как создать ваши собственные пользовательские теги, продолжайте читать.
Автоконфигурация тегов
Если вы включили автоконфигурацию , тогда некоторые теги
применяются для вас автоматически. Это так для тега twig.extension
: контейнер видит,
что ваш клас расширяет AbstractExtension
(точнее, реализует ExtensionInterface
),
и добавляет тег для вас.
Если вы хотите применять теги автоматически для ваших собственных сервисов,
используйте опцию _instanceof
:
1 2 3 4 5 6 7 8
# config/services.yaml
services:
# эта конфигурация применяется только к сервисам, созданным этим файлом
_instanceof:
# сервисы, классы которых являются экземплярами CustomInterface будут тегированы автоматически
App\Security\CustomInterface:
tags: ['app.custom_tag']
# ...
Также возможно использовать атрибут #[AutoconfigureTag]
прямо в базовом классе или
интерфейсе:
1 2 3 4 5 6 7 8 9 10
// src/Security/CustomInterface.php
namespace App\Security;
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('app.custom_tag')]
interface CustomInterface
{
// ...
}
Tip
Если вам нужно больше возможностей для автоконфигурации экземпляров вашего базового класса, вроде их ленивости, связей или вызовов, к примеру, вы можете полагаться на атрибут Autoconfigure.
Для более продвинутых потребностей, вы можете определить автоматические теги, используя метод registerForAutoconfiguration().
В приложении Symfony, вызовите этот метод в вашем классе ядра:
1 2 3 4 5 6 7 8 9 10 11 12
// src/Kernel.php
class Kernel extends BaseKernel
{
// ...
protected function build(ContainerBuilder $containerBuilder): void
{
$containerBuilder->registerForAutoconfiguration(CustomInterface::class)
->addTag('app.custom_tag')
;
}
}
В пакете Symfony, вызовите этот метод в методе load()
класса расширения пакета:
1 2 3 4 5 6 7 8 9 10 11 12
// src/DependencyInjection/MyBundleExtension.php
class MyBundleExtension extends Extension
{
// ...
public function load(array $configs, ContainerBuilder $containerBuilder): void
{
$containerBuilder->registerForAutoconfiguration(CustomInterface::class)
->addTag('app.custom_tag')
;
}
}
Создание пользовательских тегов
Теги сами по себе не изменяют функциональность ваших сервисов каким-либо образом. Но если вы захотите, вы можете попросить у строителя контейнера список всех сервисов, которые были тегированы каким-то конкретным тегом. Это полезно в пропусках компилятора, где вы можете найти эти сервисы и использовать либо изменять их каким-либо образом.
Например, если вы используете Swift Mailer, то вы можете представить, что вы
хотите реализовать "транспортную цепочку", которая является коллекцией классов,
реализующих \Swift_Transport
. Используя цепочку, вы захотите, чтобы Swift
Mailer попробовал несколько способов передачи сообщения, пока один из них не
сработает.
Для начала, определите класс TransportChain
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// src/Mail/TransportChain.php
namespace App\Mail;
class TransportChain
{
private $transports;
public function __construct()
{
$this->transports = [];
}
public function addTransport(\MailerTransport $transport): void
{
$this->transports[] = $transport;
}
}
Затем, определите цепочку как сервис:
1 2 3
# config/services.yaml
services:
App\Mail\TransportChain: ~
Определите сервисы с пользовательским тегом
Теперь вы можете захотеть, чтобы несколько из классов \Swift_Transport
были инстанциированы и добавлены в цепочку автоматически, используя метод
addTransport()
. Например, вы можете добавить следующие транспорты как
сервисы:
1 2 3 4 5 6 7 8
# config/services.yaml
services:
MailerSmtpTransport:
arguments: ['%mailer_host%']
tags: ['app.mail_transport']
MailerSendmailTransport:
tags: ['app.mail_transport']
Заметьте, что каждому сервису был предоставлен тег под названием app.mail_transport
.
Это пользовательский тег, который вы будете использовать в вашем пропуске компилятора.
Пропуск компилятора - это то, что придаёт этому тегу какой-то "смысл".
Создайте пропуск компилятора
Теперь вы можете использовать пропуск компилятора ,
чтобы запросить у контейнера любые сервисы с тегом app.mail_transport
:
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 28
// src/DependencyInjection/Compiler/MailTransportPass.php
namespace App\DependencyInjection\Compiler;
use App\Mail\TransportChain;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class MailTransportPass implements CompilerPassInterface
{
public function process(ContainerBuilder $containerBuilder): void
{
// всегда вначале проверяйте, определён ли первичный сервис
if (!$containerBuilder->has(TransportChain::class)) {
return;
}
$definition = $containerBuilder->findDefinition(TransportChain::class);
// найти все ID сервисов с тегом app.mail_transport tag
$taggedServices = $containerBuilder->findTaggedServiceIds('app.mail_transport');
foreach ($taggedServices as $id => $tags) {
// добавьте транспортный сервис в сервис ChainTransport
$definition->addMethodCall('addTransport', [new Reference($id)]);
}
}
}
Зарегистрируйте пропуск в контейнере
Для того, чтобы запустить пропуск компилятора, когда контейнер будет скомпилирован, вам нужно добавить пропуск компилятора в контейнер в расширение пакета или из вашего ядра:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// src/Kernel.php
namespace App;
use App\DependencyInjection\Compiler\MailTransportPass;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
// ...
class Kernel extends BaseKernel
{
// ...
protected function build(ContainerBuilder $containerBuilder): void
{
$containerBuilder->addCompilerPass(new MailTransportPass());
}
}
Tip
При реализации CompilerPassInterface
в расширении сервиса, вам не
нужно регистрировать его. Смотрите документацию компонентов ,
чтобы узнать больше информации.
Добавление дополнительных атрибутов в тег
Иногда вам будет нужна дополнительная информацию о каждом сервисе, который был тегирован вашим тегом. Например, вы можете захотеть добавить дополнительное имя каждому члену транспортной цепочки.
Для начала, измените класс TransportChain
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
class TransportChain
{
private $transports;
public function __construct()
{
$this->transports = [];
}
public function addTransport(\MailerTransport $transport, $alias): void
{
$this->transports[$alias] = $transport;
}
public function getTransport($alias): ?\MailerTransport
{
return $this->transports[$alias] ?? null;
}
}
Как вы видите, когда вызывается addTransport()
, требуется не только объект
Swift_Transport
, но также дополнительное имя строки для этого транспорта.
Тогда как вы можете разрешить каждому тегированному транспортному сервису также
снабжать дополнительное имя?
Чтобы ответить на этот вопрос, измените объявление сервиса:
1 2 3 4 5 6 7 8 9 10
# config/services.yaml
services:
MailerSmtpTransport:
arguments: ['%mailer_host%']
tags:
- { name: 'app.mail_transport', alias: 'smtp' }
MailerSendmailTransport:
tags:
- { name: 'app.mail_transport', alias: 'sendmail' }
Tip
В формате YAML вы можете предоставить тег как простую строку, если вам не нужно указывать дополнительные атрибуты. Следующие определения являются эквивалентными.
1 2 3 4 5 6 7 8 9 10 11 12
# config/services.yaml
services:
# Compact syntax
MailerSendmailTransport:
class: \MailerSendmailTransport
tags: ['app.mail_transport']
# Verbose syntax
MailerSendmailTransport:
class: \MailerSendmailTransport
tags:
- { name: 'app.mail_transport' }
Заметьте, что вы добавили общий ключ alias
к тегу. Чтобы действительно
использовать его, обновите компилятор:
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\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class TransportCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $containerBuilder): void
{
// ...
foreach ($taggedServices as $id => $tags) {
// сервис может иметь один и тот же тег дважды
foreach ($tags as $attributes) {
$definition->addMethodCall('addTransport', [
new Reference($id),
$attributes['alias'],
]);
}
}
}
}
Двойной цикл может быть запутанным. Это потому, что сервис может иметь
больше одного тега. Вы тегируете сервис дважды или более с помощью тега
app.mail_transport
. Второй цикл foreach повторяет набор тегов
app.mail_transport
для текущего сервиса и даёт вам атрибуты.
Ссылайтесь на тегированные сервисы
Symfony предоставляет сокращение для внедрения всех сервисов, тегированных конкретным тегом, что частно нужно в некоторых приложениях, чтобы вам не нужно было подключать пропуск компилятора только для этого.
Рассмотрите следующий класс HandlerCollection
, где вы хотите внедрить все сервисы с
тегом app.handler
в аргумент конструктора:
1 2 3 4 5 6 7 8 9
// src/HandlerCollection.php
namespace App;
class HandlerCollection
{
public function __construct(iterable $handlers)
{
}
}
Symfony позволяет вам внедрять сервисы используя конфигурацию YAML/XML/PHP или напрямую через атрибуты PHP:
1 2 3 4 5 6 7 8 9 10 11 12 13
// src/HandlerCollection.php
namespace App;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
class HandlerCollection
{
public function __construct(
// атрибут должен быть применён напрямую к аргументу для автомонтирования
#[TaggedIterator('app.handler')] iterable $handlers
) {
}
}
Если по какой-то причине вам нужно исключить один или более сервисов при использовании
тегированного итератора, добавьте опцию exclude
:
1 2 3 4 5 6 7 8 9 10 11 12
// src/HandlerCollection.php
namespace App;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
class HandlerCollection
{
public function __construct(
#[TaggedIterator('app.handler', exclude: ['App\Handler\Three'])] iterable $handlers
) {
}
}
Note
В случае ссылания на сам сервис, с тегом, который используется в тегированном итераторе, он автоматически иключается из внедрённого итерируемого.
6.1
Опция exclude
была представлена в Symfony 6.1.
6.3
Автоматическое исключение ссылающегося сервиса во внедрённом итерируемом было представлено в Symfony 6.3.
See also
Смотрите также тегированные сервисы локатора
Тегированные сервисы с приоритетностью
Тегированные сервисы могуть быть приоритизированы с использованием
атрибута priority
. Приоритетность - это положительное или отрицательное
целое число, которое по умолчанию равняется 0
. Чем выше число, тем раньше
будет найден тегированный сервис в коллекции:
1 2 3 4 5
# config/services.yaml
services:
App\Handler\One:
tags:
- { name: 'app.handler', priority: 20 }
Другой опцией. которая особенно полезна при использовании автоконфигурации тегов,
является реализация статического метода getDefaultPriority()
в самом сервисе:
1 2 3 4 5 6 7 8 9 10
// src/Handler/One.php
namespace App\Handler;
class One
{
public static function getDefaultPriority(): int
{
return 3;
}
}
Если вы хотите иметь другой метод, определяющие приоритетность
(например, getPriority()
вместо getDefaultPriority()
),
вы можете определить его в конфигурации сервиса сбора:
1 2 3 4 5 6 7 8 9 10 11 12 13
// src/HandlerCollection.php
namespace App;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
class HandlerCollection
{
public function __construct(
#[TaggedIterator('app.handler', defaultPriorityMethod: 'getPriority')]
iterable $handlers
) {
}
}
Тегированные сервисы с индексом
Если вы хотите извлечь конкретный сервис из внедренной коллекции, вы можете
использовать опции index_by
и default_index_method
аргумента, в сочетании
с !tagged_iterator
.
Используя предыдущий пример, эта конфигурация сервиса создает коллекцию,
проиндексированную по атрибуту key
:
1 2 3 4 5 6 7 8 9 10 11 12 13
// src/HandlerCollection.php
namespace App;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
class HandlerCollection
{
public function __construct(
#[TaggedIterator('app.handler', indexAttribute: 'key')]
iterable $handlers
) {
}
}
После компиляции, HandlerCollection
может итерировать поверх ваших обработчиков
приложения. Чтобы извлечь конкретный сервис из итератора, вызовите функцию
iterator_to_array()
, а затем используйте атрибут key
, чтобы получить элемент
массива. Например, чтобы извлечь обработчик handler_two
:
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
// src/Handler/One.php
namespace App\Handler;
class One
{
// ...
public static function getDefaultIndexName(): string
{
return 'handler_one';
}
}
Вы также можете определить имя статического метода, реализуемого в каждом
сервисе, с помощью атрибута ``default_index_method`` в тегированном аргументе:
.. configuration-block::
.. code-block:: php-attributes
// src/HandlerCollection.php
namespace App;
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
class HandlerCollection
{
public function __construct(
#[TaggedIterator('app.handler', defaultIndexMethod: 'getIndex')]
iterable $handlers
) {
}
}
.. code-block:: yaml
# config/services.yaml
services:
# ...
App\HandlerCollection:
# use getIndex() instead of getDefaultIndexName()
arguments: [!tagged_iterator { tag: 'app.handler', default_index_method: 'getIndex' }]
.. code-block:: xml
<!-- 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">
<!-- use getIndex() instead of getDefaultIndexName() -->
<argument type="tagged_iterator"
tag="app.handler"
default-index-method="someFunctionName"
/>
</service>
</services>
</container>
.. code-block:: php
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use App\HandlerCollection;
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
return function (ContainerConfigurator $containerConfigurator) {
$services = $containerConfigurator->services();
// ...
// использовать getIndex() вместо getDefaultIndexName()
$services->set(HandlerCollection::class)
->args([
tagged_iterator('app.handler', null, 'getIndex'),
])
;
};
Атрибут #[AsTaggedItem]
Возможно определить и приоритет и индекс тегированного объекта, благодаря
атрибуту #[AsTaggedItem]
. Этот атрибут должен быть использован прямо в
классе сервиса, который вы хотите сконфигурировать:
1 2 3 4 5 6 7 8 9 10
// src/Handler/One.php
namespace App\Handler;
use Symfony\Component\DependencyInjection\Attribute\AsTaggedItem;
#[AsTaggedItem(index: 'handler_one', priority: 10)]
class One
{
// ...
}