Как работать с тегами сервисов
Дата обновления перевода 2024-07-28
Как работать с тегами сервисов
Теги сервисов - это способ сообщить 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']
# ...
Caution
Если вы используете конфигурацию PHP, вам нужно вызвать instanceof
перед
регистрацией любого сервиса, чтобы убедиться в правильности применения тегов.
Также возможно использовать атрибут #[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 $container): void
{
$container->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 $container): void
{
$container->registerForAutoconfiguration(CustomInterface::class)
->addTag('app.custom_tag')
;
}
}
Регистрация автоконфигурации не ограничивается интерфейсами. Можно использовать атрибуты PHP для автоконфигурирования сервисов с помощью метода registerAttributeForAutoconfiguration():
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
// src/Attribute/SensitiveElement.php
namespace App\Attribute;
#[\Attribute(\Attribute::TARGET_CLASS)]
class SensitiveElement
{
public function __construct(
private string $token,
) {
}
public function getToken(): string
{
return $this->token;
}
}
// src/Kernel.php
use App\Attribute\SensitiveElement;
class Kernel extends BaseKernel
{
// ...
protected function build(ContainerBuilder $container): void
{
// ...
$container->registerAttributeForAutoconfiguration(SensitiveElement::class, static function (ChildDefinition $definition, SensitiveElement $attribute, \ReflectionClass $reflector): void {
// Применить тег 'app.sensitive_element' ко всем классам с атрибутом SensitiveElement,
// и присоединить значение токена к тегу
$definition->addTag('app.sensitive_element', ['token' => $attribute->getToken()]);
});
}
}
Вы также можете сделать атрибуты пригодными для использования в методах. Для этого обновите предыдущий
пример и добавьте Атрибут::TARGET_METHOD
:
1 2 3 4 5 6 7 8
// src/Attribute/SensitiveElement.php
namespace App\Attribute;
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
class SensitiveElement
{
// ...
}
Затем обновите registerAttributeForAutoconfiguration()
для поддержки ReflectionMethod
:
// src/Kernel.php use AppAttributeSensitiveElement;
class Kernel extends BaseKernel { // ...
protected function build(ContainerBuilder $container): void { // ...
- $container->registerAttributeForAutoconfiguration(SensitiveElement::class, static function (
- ChildDefinition $definition, SensitiveElement $attribute, // обновить тип соединения для поддержки нескольких типов отображения // ви также можете использовать интерфейс "Reflector" ReflectionClass|ReflectionMethod $reflector): void { if ($reflection instanceof ReflectionMethod) { // ... } }
);
}
}
Tip
Вы также можете определить атрибут, который будет использоваться для свойств и параметров с помощью
Attribute::TARGET_PROPERTY
и Attribute::TARGET_PARAMETER
; затем поддерживайте
ReflectionProperty
и ReflectionParameter
в вашем вызываемом
registerAttributeForAutoconfiguration().
Создание пользовательских тегов
Теги сами по себе не изменяют функциональность ваших сервисов каким-либо образом. Но если вы захотите, вы можете попросить у строителя контейнера список всех сервисов, которые были тегированы каким-то конкретным тегом. Это полезно в пропусках компилятора, где вы можете найти эти сервисы и использовать либо изменять их каким-либо образом.
Например, если вы используете Swift Mailer, то вы можете представить, что вы
хотите реализовать "транспортную цепочку", которая является коллекцией классов,
реализующих \Swift_Transport
. Используя цепочку, вы захотите, чтобы Swift
Mailer попробовал несколько способов передачи сообщения, пока один из них не
сработает.
Для начала, определите класс TransportChain
:
1 2 3 4 5 6 7 8 9 10 11 12
// src/Mail/TransportChain.php
namespace App\Mail;
class TransportChain
{
private array $transports = [];
public function addTransport(\MailerTransport $transport): void
{
$this->transports[] = $transport;
}
}
Then, define the chain as a service:
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 $container): void
{
$container->addCompilerPass(new MailTransportPass());
}
}
Tip
При реализации CompilerPassInterface
в расширении сервиса, вам не
нужно регистрировать его. Смотрите документацию компонентов ,
чтобы узнать больше информации.
Добавление дополнительных атрибутов в теги
Иногда вам будет нужна дополнительная информацию о каждом сервисе, который был тегирован вашим тегом. Например, вы можете захотеть добавить дополнительное имя каждому члену транспортной цепочки.
Для начала, измените класс TransportChain
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
class TransportChain
{
private array $transports = [];
public function addTransport(\MailerTransport $transport, $alias): void
{
$this->transports[$alias] = $transport;
}
public function getTransport($alias): ?\MailerTransport
{
return $this->transports[$alias] ?? null;
}
}
Как вы видите, когда вызывается addTransport()
, требуется не только объект
MailerTransport
, но также дополнительное имя строки для этого транспорта.
Тогда как вы можете разрешить каждому тегированному транспортному сервису также
снабжать дополнительное имя?
Чтобы ответить на этот вопрос, измените объявление сервиса:
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', 'anotherAlias']}
Tip
Атрибут name
используется по умолчанию для определения имени тега.
Если вы хотите добавить атрибут name
к какому-либо тегу в форматах XML или YAML,
необходимо использовать этот специальный синтаксис:
1 2 3 4 5 6 7 8 9
# config/services.yaml
services:
MailerSmtpTransport:
arguments: ['%mailer_host%']
tags:
# это тег под названием 'app.mail_transport'
- { name: 'app.mail_transport', alias: 'smtp' }
# это тег под названием 'app.mail_transport' with two attributes ('name' and 'alias')
- app.mail_transport: { name: 'arbitrary-value', alias: 'smtp' }
Tip
В формате YAML вы можете предоставить тег как простую строку, если вам не нужно указывать дополнительные атрибуты. Следующие определения являются эквивалентными.
1 2 3 4 5 6 7 8 9 10 11 12
# config/services.yaml
services:
# Компактный синтаксис
MailerSendmailTransport:
class: \MailerSendmailTransport
tags: ['app.mail_transport']
# Многословный синтаксис
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 $container): 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
) {
}
}
Note
Некоторые IDE выдают ошибку при использовании #[TaggedIterator]
вместе с
с продвижением конструктора PHP
: "Атрибут не может быть применен к свойству,
так как не содержит флажка 'Attribute::TARGET_PROPERTY'". Причина в том, что
эти аргументы конструктора являются одновременно параметрами и свойствами класса. Вы можете смело игнорировать это сообщение об ошибке.
Если по какой-то причине вам нужно исключить один или более сервисов при использовании
тегированного итератора, добавьте опцию exclude
:
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', exclude: ['App\Handler\Three'])]
iterable $handlers
) {
}
}
В случае, если ссылающийся сервис сам помечен тегом, используемым в тегированном
итераторе, он автоматически исключается из внедряемого итератора. Это поведение можно
отключить, установив опцию exclude_self
в значение false
:
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', exclude: ['App\Handler\Three'], excludeSelf: false)]
iterable $handlers
) {
}
}
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
) {
}
}
Тегированные сервисы с индексом
По умолчанию тегированные сервисы индексируются с использованием ID сервисов. Вы можете
изменить это поведение с помощью двух опций тегированного (index_by
и
default_index_method
), которые можно использовать независимо или вместе.
Опция index_by
/ indexAttribute
Эта опция определяет имя опции/атрибута, в котором хранится значение, используемое для индексации сервисов:
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
) {
}
}
В этом примере опцией index_by
является key
. Все сервисы определяют эту
опцию/атрибут, поэтому это значение будет использоваться для индексации сервисов. Например,
чтобы получить сервис App\Handler\Two
:
1 2 3 4 5 6 7 8 9 10 11 12 13
// src/Handler/HandlerCollection.php
namespace App\Handler;
class HandlerCollection
{
public function __construct(iterable $handlers)
{
$handlers = $handlers instanceof \Traversable ? iterator_to_array($handlers) : $handlers;
// это значение определяется опцией сервиса `key`
$handlerTwo = $handlers['handler_two'];
}
}
Если какой-то сервис не определяет опцию/атрибут, сконфигурированный в index_by
,
Symfony применяет этот резервный процесс:
- Если класс сервиса определяет статический метод с именем
getDefault<CamelCase index_by value>Name
(в данном примереgetDefaultKeyName()
), вызовите его и используйте возвращенное значение; - В противном случае вернитесь к поведению по умолчанию и используйте идентификатор сервиса.
Опция default_index_method
Эта опция определяет имя метода класса сервиса, который будет вызван для получения значения, используемого для индексации сервисов:
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', defaultIndexMethod: 'getIndex')]
iterable $handlers
) {
}
}
Если какой-то класс сервиса не определяет метод, сконфигурированный в default_index_method
,
Symfony вернется к использованию идентификатора сервиса в качестве индекса внутри тегированных сер
Объединение опций index_by
и default_index_method
Вы можете объединить оба варианта в одной коллекции тегированных сервисов. Symfony будет обрабатывать их в следующем порядке:
- Если сервис определяет опцию/атрибут, сконфигурированный в
index_by
, используйте его; - Если класс сервиса определяет метод, сконфигурированный в
default_index_method
, используйте его; - В противном случае вернитесь к использованию идентификатора сервиса в качестве его индекса в коллекции тегированных сервисов.
Атрибут #[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
{
// ...
}