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

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

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

Теги сервісів - це спосіб повідомити Symfony або іншим стороннім пакетам, що ваш сервіс має бути зареєстрований якимось особливим чином. Візьміть наступний приклад:

  • YAML
  • XML
  • PHP
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:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
# config/services.yaml
services:
    # ця конфігурація застосовується лише до сервісів, створених цим файлом
    _instanceof:
        # сервіси, класи яких є екземплярами CustomInterface будуть теговані автоматично
        App\Security\CustomInterface:
            tags: ['app.custom_tag']
    # ...

Для більш просунутих потреб, ви можете визначити автоматичні теги, використовуючи метод 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')
        ;
    }
}

Створення користувацьких тегів

Теги самі по собі не змінюють функціональність ваших сервісів ніяким чином. Але якщо ви захочете, ви можете попросити у будівельника контейнера список всіх сервісів, які були теговані якимось конкретним тегом. Це корисно у пропусках компілятора, де можете знайти ці сервіси та використати або змінити їх якимось чином.

Наприклад, якщо ви використовуєте 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(\Swift_Transport $transport): void
    {
        $this->transports[] = $transport;
    }
}

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

  • YAML
  • XML
  • PHP
1
2
3
# config/services.yaml
services:
    App\Mail\TransportChain: ~

Визначте сервіси з користувацьким тегом

Тепер ви можете захотіти, щоб декілька з класів \Swift_Transport були інстанційовані та додані у ланцюжок автоматично, використовуючи метод addTransport(). Наприклад, ви можете додати наступні транспорти як сервіси:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
# config/services.yaml
services:
    Swift_SmtpTransport:
        arguments: ['%mailer_host%']
        tags: ['app.mail_transport']

    Swift_SendmailTransport:
        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
15
16
17
18
19
20
21
22
23
class TransportChain
{
    private $transports;

    public function __construct()
    {
        $this->transports = [];
    }

    public function addTransport(\Swift_Transport $transport, $alias): void
    {
        $this->transports[$alias] = $transport;
    }

    public function getTransport($alias): ?\Swift_Transport
    {
        if (array_key_exists($alias, $this->transports)) {
            return $this->transports[$alias];
        }

        return null;
    }
}

Як ви бачите, коли викликається addTransport(), потрібний не тільки об'єкт Swift_Transport, але також додаткове ім'я рядка для цього транспорту. Тоді як ви можете дозволити кожному тегованому транспортному сервісу також постачати додаткове ім'я?

Щоб відповісти на це запитання, змініть оголошення сервісу:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
# config/services.yaml
services:
    Swift_SmtpTransport:
        arguments: ['%mailer_host%']
        tags:
            - { name: 'app.mail_transport', alias: 'smtp' }

    Swift_SendmailTransport:
        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
    Swift_SendmailTransport:
        class: \Swift_SendmailTransport
        tags: ['app.mail_transport']

    # Verbose syntax
    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 надає скорочення для впровадження всіх сервісів, тегованих конкретним тегом, що часто потрібно у деяких додатках, щоб вам не потрібно було підключати пропуск компілятора лише для цього.

У наступному прикладі всі сервіси, теговані app.handler передаються як перший аргумент конструктора сервісу App\HandlerCollection:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
11
12
# config/services.yaml
services:
    App\Handler\One:
        tags: ['app.handler']

    App\Handler\Two:
        tags: ['app.handler']

    App\HandlerCollection:
        # впровадити всі сервіси, теговані app.handler в якості першого аргументу
        arguments:
            - !tagged_iterator app.handler

Після компіляції, сервіс HandlerCollection має можливість ітерувати над вашими обробниками додатку:

1
2
3
4
5
6
7
8
9
// src/HandlerCollection.php
namespace App;

class HandlerCollection
{
    public function __construct(iterable $handlers)
    {
    }
}

See also

Дивіться також теговані сервіси локатора

Теговані сервіси з пріоритетністю

Теговані сервіси можуть бути пріоритизовані з використанням атрибута priority. Пріоритетність - це додаткове або від'ємне ціле число, яке за замовчуванням дорівнює 0. Чим більше число, тим раніше буде знайдено тегований сервіс у колекції:

  • YAML
  • XML
  • PHP
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()), ви можете визначити його у конфігурації сервісу збору:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
# config/services.yaml
services:
    App\HandlerCollection:
        # впровадити всі сервіси з тегом app.handler в якості першого аргументу
        arguments:
            - !tagged_iterator { tag: app.handler, default_priority_method: getPriority }

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

Якщо ви хочете отримати конкретний сервіс із впровадженої колекції, ви можете використати опції index_by і default_index_method аргумента, у поєднанні з !tagged_iterator.

Використовуючи попередній приклад, ця конфігурація сервісу створює колекцію, проіндексовану за атрибутом key:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
11
12
# config/services.yaml
services:
    App\Handler\One:
        tags:
            - { name: 'app.handler', key: 'handler_one' }

    App\Handler\Two:
        tags:
            - { name: 'app.handler', key: 'handler_two' }

    App\HandlerCollection:
        arguments: [!tagged_iterator { tag: 'app.handler', index_by: 'key' }]

Після компіляції, HandlerCollection може ітерувати над вашими обробниками додатку. Щоб отримати конкретний сервіс з ітератору, викличте функцію iterator_to_array(), а потім використайте атрибут key, щоб отримати елемент масиву. Наприклад, щоб отримати обробник handler_two:

1
2
3
4
5
6
7
8
9
10
11
12
// src/Handler/HandlerCollection.php
namespace App\Handler;

class HandlerCollection
{
    public function __construct(iterable $handlers)
    {
        $handlers = $handlers instanceof \Traversable ? iterator_to_array($handlers) : $handlers;

        $handlerTwo = $handlers['handler_two'];
    }
}

Tip

Як і з пріоритетністю, ви можете також реалізувати статичний метод getDefaultIndexName() в обробниках та опустити атрибут індексу (key):

1
2
3
4
5
6
7
8
9
10
11
// src/Handler/One.php
namespace App\Handler;

class One
{
    // ...
    public static function getDefaultIndexName(): string
    {
        return 'handler_one';
    }
}

Ви також можете визначити ім'я статичного методу, який реалізовано в кожному сервісі, за допомогою атрибута default_index_method у тегованому аргументі:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
# config/services.yaml
services:
    # ...

    App\HandlerCollection:
        # використовуйте getIndex() замість getDefaultIndexName()
        arguments: [!tagged_iterator { tag: 'app.handler', default_index_method: 'getIndex' }]