Как работать с тегами сервисов

Дата обновления перевода 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 применяет этот резервный процесс:

  1. Если класс сервиса определяет статический метод с именем getDefault<CamelCase index_by value>Name (в данном примере getDefaultKeyName()), вызовите его и используйте возвращенное значение;
  2. В противном случае вернитесь к поведению по умолчанию и используйте идентификатор сервиса.

Опция 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 будет обрабатывать их в следующем порядке:

  1. Если сервис определяет опцию/атрибут, сконфигурированный в index_by, используйте его;
  2. Если класс сервиса определяет метод, сконфигурированный в default_index_method, используйте его;
  3. В противном случае вернитесь к использованию идентификатора сервиса в качестве его индекса в коллекции тегированных сервисов.

Атрибут #[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
{
    // ...
}