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

Так же, как запись блога в сети может быть тегирован такими тегами, как "Symfony" или "PHP", сервисы, сконфигурированные в вашем контейнере тоже могут быть тегированы. В сервис-контейнере тег подразумевает, что сервис должен быть использован для какой-то конкретной цели. Возьмём следующий пример:

  • YAML
    1
    2
    3
    4
    5
    # app/config/services.yml
    services:
        AppBundle\Twig\AppExtension:
            public: false
            tags: [twig.extension]
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    <!-- app/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
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="AppBundle\Twig\AppExtension" public="false">
                <tag name="twig.extension" />
            </service>
        </services>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    // app/config/services.php
    use AppBundle\Twig\AppExtension;
    
    $container->register('app.twig_extension', AppExtension::class)
        ->setPublic(false)
        ->addTag('twig.extension');
    

Тег twig.extension - это специальный тег, который TwigBundle использует во время конфигурации. Назначив сервису этот тег twig.extension, пакет знает, что сервис AppExtension::class должен быть зарегистрирован в Twig как расширение Twig. Другими словами, Twig находит все сервисы, тегированные twig.extension и автоматически регистрирует их как расширения.

Потом теги становятся способом сообщить Symfony или другим стороним пакетам. что ваш сервис должен быть зарегистрирован или как-то особенно использован пакетом.

Чтобы получить список всех доступных в базовом фреймворке Symfony тегов, смотрите Built-in Symfony Service Tags. Каждый из них имеет своё влияние на ваш сервис и многие теги требуют дополнительных аргументов (кроме простого параметра name).

Для большинства пользователей - это всё, что вам нужно знать. Если вы хотите углубиться в изучение того, как создать ваши собственные пользовательские теги, то продолжайте читать.

Автоконфигурация тегов

Начиная с Symfony 3.3, если вы включите автоконфигурацию, то некоторые теги применяются для вас самостоятельно. Это применимо к тегу twig.extension: контейнер видит, что ваш класс раширяет Twig_Extension (или, точнее, что он реализует Twig_ExtensionInterface) и добавляет тег за вас.

Tip

Чтобы применить тег ко всем вашим автоматически сконфигурированным сервисам, расширяющим класс или интерфейс, вызовите метод registerForAutoconfiguration() в extension или из вашего ядра:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// app/AppKernel.php
class AppKernel extends Kernel
{
    // ...

    protected function build(ContainerBuilder $container)
    {
        $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/AppBundle/Mail/TransportChain.php
namespace AppBundle\Mail;

class TransportChain
{
    private $transports;

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

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

Потом, определите цепочку как сервис:

  • YAML
    1
    2
    services:
        AppBundle\Mail\TransportChain: ~
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    <?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
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="AppBundle\Mail\TransportChain" />
        </services>
    </container>
    
  • PHP
    1
    2
    3
    use AppBundle\Mail\TransportChain;
    
    $container->autowire(TransportChain::class);
    

Определеите сервисы с пользовательским тегом

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

  • YAML
    1
    2
    3
    4
    5
    6
    7
    services:
        Swift_SmtpTransport:
            arguments: ['%mailer_host%']
            tags: [app.mail_transport]
    
        Swift_SendmailTransport:
            tags: [app.mail_transport]
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?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
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="Swift_SmtpTransport">
                <argument>%mailer_host%</argument>
    
                <tag name="app.mail_transport" />
            </service>
    
            <service class="\Swift_SendmailTransport">
                <tag name="app.mail_transport" />
            </service>
        </services>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    $container->register(\Swift_SmtpTransport::class)
        ->addArgument('%mailer_host%')
        ->addTag('app.mail_transport');
    
    $container->register(\Swift_SendmailTransport::class)
        ->addTag('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/AppBundle/DependencyInjection/Compiler/MailTransportPass.php
namespace AppBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
use AppBundle\Mail\TransportChain;

class MailTransportPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        // всегда вначале проверяйте, определён ли первичный сервис
        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', array(new Reference($id)));
        }
    }
}

Зарегистрируйте пропуск в контейнере

Для того, чтобы запустить пропуск компилятора, когда контейнер будет скомпилирован, вам нужно добавить пропуск в контейнер в методе build() вашего пакета:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// src/AppBundle/AppBundle.php

// ...
use Symfony\Component\DependencyInjection\ContainerBuilder;
use AppBundle\DependencyInjection\Compiler\MailTransportPass;

class AppBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        $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
class TransportChain
{
    private $transports;

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

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

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

Как вы видите, когда вызывается addTransport(), требуется не только объект Swift_Transport, но также дополнительное имя строки для этого транспорта. Тогда как вы можете разрешить каждому тегированному транспортному сервису также снабжать дополнительное имя?

Чтобы ответить на этот вопрос, измените объявление сервиса:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    8
    9
    services:
        Swift_SmtpTransport:
            arguments: ['%mailer_host%']
            tags:
                - { name: app.mail_transport, alias: smtp }
    
        Swift_SendmailTransport:
            tags:
                - { name: app.mail_transport, alias: sendmail }
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?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
            http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <services>
            <service id="Swift_SmtpTransport">
                <argument>%mailer_host%</argument>
    
                <tag name="app.mail_transport" alias="smtp" />
            </service>
    
            <service id="Swift_SendmailTransport">
                <tag name="app.mail_transport" alias="sendmail" />
            </service>
        </services>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    $container->register(\Swift_SmtpTransport::class)
        ->addArgument('%mailer_host%')
        ->addTag('app.mail_transport', array('alias' => 'foo'));
    
    $container->register(\Swift_SendmailTransport::class)
        ->addTag('app.mail_transport', array('alias' => 'bar'));
    

Tip

В формате YAML, вы можете представить тег в качестве простой строки, если вам не нужно указывать дополнительные атрибуты. Следующие определения являются эквивалентными.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
services:

    # синтаксис Compact
    Swift_SendmailTransport:
        class: \Swift_SendmailTransport
        tags: [app.mail_transport]

    # синтаксиси Verbose
    Swift_SendmailTransport:
        class: \Swift_SendmailTransport
        tags:
            - { name: app.mail_transport }

New in version 3.3: Поддержка компактных нотаций тега в формате YAML была представлена в Symfony 3.3.

Отметьте, что вы добавили общий ключ 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\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;

class TransportCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        // ...

        foreach ($taggedServices as $id => $tags) {

            // сервис может иметь один и тот же тег дважды
            foreach ($tags as $attributes) {
                $definition->addMethodCall('addTransport', array(
                    new Reference($id),
                    $attributes["alias"]
                ));
            }
        }
    }
}

Двойной цикл может быть запутанным. Это потому, что сервис может иметь больше одного тега. Вы тегируете сервис дважды или более с помощью тега app.mail_transport. Второй цикл foreach повторяет набор тегов app.mail_transport для текущего сервиса и даёт вам атрибуты.

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.