如何操作服务的标签

3.4 版本
维护中的版本

与网上的博客主题被打上诸如“Symfony”或“PHP”这种标签相类似,配置在容器中的服务也可以打上标签。在容器中,标签即意味着服务应当被用于一个特定目的。看下面的代码:

1
2
3
4
5
6
7
# app/config/services.yml
services:
    foo.twig.extension:
        class: AppBundle\Extension\FooExtension
        public: false
        tags:
            - { name: twig.extension }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 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="foo.twig.extension"
            class="AppBundle\Extension\FooExtension"
            public="false">
 
            <tag name="twig.extension" />
        </service>
    </services>
</container>
1
2
3
4
5
6
7
// app/config/services.php
use Symfony\Component\DependencyInjection\Definition;
 
$definition = new Definition('AppBundle\Extension\FooExtension');
$definition->setPublic(false);
$definition->addTag('twig.extension');
$container->setDefinition('foo.twig.extension', $definition);

twig.extension标签是一个特殊的tag,TwigBundle会在配置信息(被解析的过程)中用到。通过给服务打上twig.extension标签,TwigBundle就知道有个foo.twig.extension服务应该被注册成Twig引擎中的扩展。换言之,Twig将找到所有打了twig.extension标签的服务,然后自动将它们注册为扩展。

至于标签本身,是作用于Symfony或其他第三方扩展之bundle的“通知方式”,即,你的服务应该被“注册到”或是“以某种特殊方式使用在”那个bundle之中。

Symfony框架核心所用到的全部标签列表,参考注入之标签。那里面的每一种标签对于你的服务来说都有着不同的作用,很多tag还需要附加参数(指的是除了name之外的parameter)。

创建自定义标签 

标签(tags)就是通常的字符串(也可能带有附加选项)。标签可以应用到任何服务中。标签本身不以任何方式对服务发生作用。但如果你选择使用标签,你可以请求服务容器把所有具有相同标签的服务罗列出来。这在编译器传递(compiler pass)过程中特别有用,因为你可以找到这些服务并对其进行某种方式的修改。

例如:当你使用Swift Mailer邮件服务时,你可能需要执行一个“传送链”(transport chain),这是一个类的集合,每个类都实现\Swift _Transport。通过这个chain,你可以令Swift Mailer尝试多种不同的信息传送方式,直到信息发送成功。

首先,先定义一个TransportChain类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TransportChain
{
    private $transports;
 
    public function __construct()
    {
        $this->transports = array();
    }
 
    public function addTransport(\Swift_Transport $transport)
    {
        $this->transports[] = $transport;
    }
}

然后,将chain定义为服务:

1
2
3
services:
    acme_mailer.transport_chain:
        class: TransportChain
1
2
3
4
5
6
7
8
9
<?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="acme_mailer.transport_chain" class="TransportChain" />
    </services>
</container>
1
$container->register('acme_mailer.transport_chain', 'TransportChain');

用自定义标签定义服务 

现在你需要将若干\Swift_Transport类进行实例化,然后添加到chain中,这个过程是通过addTransport()方法自动完成的。举例来说,你应该先把以下的transports设为服务:

1
2
3
4
5
6
7
8
9
10
11
services:
    acme_mailer.transport.smtp:
        class: \Swift_SmtpTransport
        arguments:
            - '%mailer_host%'
        tags:
            -  { name: acme_mailer.transport }
    acme_mailer.transport.sendmail:
        class: \Swift_SendmailTransport
        tags:
            -  { name: acme_mailer.transport }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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="acme_mailer.transport.smtp" class="\Swift_SmtpTransport">
            <argument>%mailer_host%</argument>
            <tag name="acme_mailer.transport" />
        </service>
 
        <service id="acme_mailer.transport.sendmail" class="\Swift_SendmailTransport">
            <tag name="acme_mailer.transport" />
        </service>
    </services>
</container>
1
2
3
4
5
6
7
8
9
use Symfony\Component\DependencyInjection\Definition;
 
$definitionSmtp = new Definition('\Swift_SmtpTransport', array('%mailer_host%'));
$definitionSmtp->addTag('acme_mailer.transport');
$container->setDefinition('acme_mailer.transport.smtp', $definitionSmtp);
 
$definitionSendmail = new Definition('\Swift_SendmailTransport');
$definitionSendmail->addTag('acme_mailer.transport');
$container->setDefinition('acme_mailer.transport.sendmail', $definitionSendmail);

注意上面两个服务,每个都有一个名为acme_mailer.transport的标签。这是个自定义的标签,你可以使用在compiler pass中。Compiler Pass就是使这个标签变得“有意义”的东西。

编写Compiler Pass 

现在你可以使用一个compiler pass来请求服务容器提供指定标签(acme_mailer.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
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)
    {
        if (!$container->has('acme_mailer.transport_chain')) {
            return;
        }
 
        $definition = $container->findDefinition(
            'acme_mailer.transport_chain'
        );
 
        $taggedServices = $container->findTaggedServiceIds(
            'acme_mailer.transport'
        );
        foreach ($taggedServices as $id => $tags) {
            $definition->addMethodCall(
                'addTransport',
                array(new Reference($id))
            );
        }
    }
}

process()方法先是用来检查acme_mailer.transport_chain服务是否存在,然后寻找所有被打上acme_mailer.transport标签的服务。接下来,这些服务被添加到acme_mailer.transport_chain的服务定义中,每一个被找到的服务都是通过调用定义中的addTransport()方法的来完成添加的。每次调用该方法时,第一个参数即是mailer transport服务本身。

将Pass注册给Container 

完成编译器传递之后,你需要注册这个传递给服务容器。然后,容器在被编译的时候,完成传递的动作:

1
2
3
4
use Symfony\Component\DependencyInjection\ContainerBuilder;
 
$container = new ContainerBuilder();
$container->addCompilerPass(new TransportCompilerPass());

如果你使用完整版框架,编译器传递将会有不同的注册方式。参考 如何在Bundles中对Compiler Passes进行操作

当在一个服务扩展中实现CompilerPassInterface接口时,毋须再注册pass。参考中文DI组件文档 在编译过程中执行代码

对标签添加附加属性 

有些时候你需要得到每一个“带标签”服务的附加信息。接上例,你可能需要给每一个transport chain传送链中的成员添加一个假名(alias)。

修改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对象,还有一个transport假名的字符串。那么,如何才能让每个打了标签的transport服务也应用相应的假名呢?

要回答这个问题,先修改服务定义如下:

1
2
3
4
5
6
7
8
9
10
11
services:
    acme_mailer.transport.smtp:
        class: \Swift_SmtpTransport
        arguments:
            - '%mailer_host%'
        tags:
            -  { name: acme_mailer.transport, alias: foo }
    acme_mailer.transport.sendmail:
        class: \Swift_SendmailTransport
        tags:
            -  { name: acme_mailer.transport, alias: bar }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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="acme_mailer.transport.smtp" class="\Swift_SmtpTransport">
            <argument>%mailer_host%</argument>
            <tag name="acme_mailer.transport" alias="foo" />
        </service>
 
        <service id="acme_mailer.transport.sendmail" class="\Swift_SendmailTransport">
            <tag name="acme_mailer.transport" alias="bar" />
        </service>
    </services>
</container>
1
2
3
4
5
6
7
8
9
use Symfony\Component\DependencyInjection\Definition;
 
$definitionSmtp = new Definition('\Swift_SmtpTransport', array('%mailer_host%'));
$definitionSmtp->addTag('acme_mailer.transport', array('alias' => 'foo'));
$container->setDefinition('acme_mailer.transport.smtp', $definitionSmtp);
 
$definitionSendmail = new Definition('\Swift_SendmailTransport');
$definitionSendmail->addTag('acme_mailer.transport', array('alias' => 'bar'));
$container->setDefinition('acme_mailer.transport.sendmail', $definitionSendmail);

注意你刚才添加了通用的alias属性到标签中。为了真正用到它,还需要更改compiler:

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
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)
    {
        if (!$container->hasDefinition('acme_mailer.transport_chain')) {
            return;
        }
 
        $definition = $container->getDefinition(
            'acme_mailer.transport_chain'
        );
 
        $taggedServices = $container->findTaggedServiceIds(
            'acme_mailer.transport'
        );
        foreach ($taggedServices as $id => $tags) {
            foreach ($tags as $attributes) {
                $definition->addMethodCall(
                    'addTransport',
                    array(new Reference($id), $attributes["alias"])
                );
            }
        }
    }
}

双循环可能略显疑惑,这是因为一个服务可以有多个标签。之前你对两个服务(或更多)打上了acme_mailer.transport标签。第二重循环只是在遍历当前服务的acme_mailer.transport标签本身,你得到的是属性数组。

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)