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

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