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

Дата обновления перевода 2022-02-01

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

Теги сервисов - это способ сообщить 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 tag
        $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' }]