Як працювати з тегами сервісів

Дата оновлення перекладу 2024-06-06

Як працювати з тегами сервісів

Теги сервісів - це спосіб повідомити 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()]);
        });
    }
}

Ви також можете зробити атрибути доступними в методах. Щоб зробити це, оновіть попередній приклад і додайте Attribute::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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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,
            // оновити тип обʼєднання для підтримки багатьох типів відображення
            // ви також можете використати інтерфейс "\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;
    }
}

Потім визначте ланцюжок як сервіс:

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 $container): void
    {
        // завжди спочатку перевіряйте, чи визначений первинний сервіс
        if (!$container->has(TransportChain::class)) {
            return;
        }

        $definition = $container->findDefinition(TransportChain::class);

        // знайти всі ID сервісів з тегом app.mail_transport
        $taggedServices = $container->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(), потрібний не тільки об'єкт 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', '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' з двома атрибутами ('name' та '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:
    # Компактний синтасис
    Swift_SendmailTransport:
        class: \Swift_SendmailTransport
        tags: ['app.mail_transport']

    # Розширений синтаксис
    Swift_SendmailTransport:
        class: \Swift_SendmailTransport
        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
    ) {
    }
}

Теговані сервіси з індексом

За замовчуванням теговані сервіси індексуються за їхніми ідентифікаторами. Ви можете змінити цю поведінку за допомогою двох опцій тегованого ітератора (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
{
    // ...
}