跳到内容

Service Tags 使用指南

编辑此页

服务标签 是一种告诉 Symfony 或其他第三方 bundle,你的服务应该以某种特殊方式注册的方法。以下面的例子为例

1
2
3
4
# config/services.yaml
services:
    App\Twig\AppExtension:
        tags: ['twig.extension']

使用 twig.extension 标签标记的服务在 TwigBundle 初始化期间被收集,并作为扩展添加到 Twig 中。

其他标签用于将你的服务集成到其他系统中。有关核心 Symfony 框架中所有可用标签的列表,请查看 内置 Symfony 服务标签。 这些标签中的每一个都对你的服务有不同的影响,并且许多标签需要额外的参数(超出 name 参数)。

对于大多数用户来说,这就是你需要知道的全部内容。如果你想进一步了解如何创建自己的自定义标签,请继续阅读。

自动配置标签

如果你启用 autoconfigure,那么一些标签会自动为你应用。 twig.extension 标签就是这种情况:容器看到你的类扩展了 AbstractExtension (或者更准确地说,它实现了 ExtensionInterface),并为你添加了标签。

如果你想为自己的服务自动应用标签,请使用 _instanceof 选项

1
2
3
4
5
6
7
8
# config/services.yaml
services:
    # this config only applies to the services created by this file
    _instanceof:
        # services whose classes are instances of CustomInterface will be tagged automatically
        App\Security\CustomInterface:
            tags: ['app.custom_tag']
    # ...

警告

如果你正在使用 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
{
    // ...
}

提示

如果你需要更多功能来自动配置基类的实例,例如它们的惰性、绑定或调用,你可以依赖 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 bundle 中,在 bundle 扩展类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 {
            // Apply the 'app.sensitive_element' tag to all classes with SensitiveElement
            // attribute, and attach the token value to the tag
            $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,
            // update the union type to support multiple types of reflection
            // you can also use the "\Reflector" interface
            \ReflectionClass|\ReflectionMethod $reflector): void {
                if ($reflector instanceof \ReflectionMethod) {
                    // ...
                }
            }
        );
    }
}

提示

你还可以定义一个属性,使其可用于属性和参数,使用 Attribute::TARGET_PROPERTYAttribute::TARGET_PARAMETER;然后在你的 registerAttributeForAutoconfiguration() 可调用对象中支持 ReflectionPropertyReflectionParameter

创建自定义标签

标签本身实际上不会以任何方式改变你的服务的功能。 但是,如果你选择这样做,你可以向容器构建器请求使用某些特定标签标记的所有服务的列表。 这在编译器 pass 中很有用,你可以在其中找到这些服务,并以某种特定方式使用或修改它们。

例如,如果你正在使用 Symfony Mailer 组件,你可能想要实现一个 “传输链”,它是一个实现 \MailerTransport 的类的集合。 使用该链,你将希望 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: ~

使用自定义标签定义服务

现在你可能希望实例化几个 \MailerTransport 类,并使用 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 的标签。 这是你将在编译器 pass 中使用的自定义标签。 编译器 pass 是使此标签 “有意义” 的东西。

创建编译器 Pass

你现在可以使用 编译器 pass 来向容器请求任何带有 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
    {
        // always first check if the primary service is defined
        if (!$container->has(TransportChain::class)) {
            return;
        }

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

        // find all service IDs with the app.mail_transport tag
        $taggedServices = $container->findTaggedServiceIds('app.mail_transport');

        foreach ($taggedServices as $id => $tags) {
            // add the transport service to the TransportChain service
            $definition->addMethodCall('addTransport', [new Reference($id)]);
        }
    }
}

向容器注册 Pass

为了在编译容器时运行编译器 pass,你必须在 bundle 扩展或从你的内核中将编译器 pass 添加到容器中

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());
    }
}

提示

当在服务扩展中实现 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() 时,它不仅接受一个 MailerTransport 对象,还接受该传输的字符串别名。 那么,你如何允许每个标记的传输服务也提供别名呢?

要回答这个问题,请更改服务声明

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']}

提示

name 属性默认用于定义标签的名称。 如果你想在 XML 或 YAML 格式的某些标签中添加 name 属性,则需要使用此特殊语法

1
2
3
4
5
6
7
8
9
# config/services.yaml
services:
    MailerSmtpTransport:
        arguments: ['%mailer_host%']
        tags:
            # this is a tag called 'app.mail_transport'
            - { name: 'app.mail_transport', alias: 'smtp' }
            # this is a tag called 'app.mail_transport' with two attributes ('name' and 'alias')
            - app.mail_transport: { name: 'arbitrary-value', alias: 'smtp' }

提示

在 YAML 格式中,只要你不需要指定其他属性,你就可以将标签作为简单字符串提供。 以下定义是等效的。

1
2
3
4
5
6
7
8
9
10
11
12
# config/services.yaml
services:
    # Compact syntax
    MailerSendmailTransport:
        class: \MailerSendmailTransport
        tags: ['app.mail_transport']

    # Verbose syntax
    MailerSendmailTransport:
        class: \MailerSendmailTransport
        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) {

            // a service could have the same tag twice
            foreach ($tags as $attributes) {
                $definition->addMethodCall('addTransport', [
                    new Reference($id),
                    $attributes['alias'],
                ]);
            }
        }
    }
}

双循环可能会令人困惑。 这是因为一个服务可以有多个标签。 你可以使用 app.mail_transport 标签两次或多次标记一个服务。 第二个 foreach 循环迭代为当前服务设置的 app.mail_transport 标签,并为你提供属性。

引用已标记的服务

Symfony 提供了一个快捷方式来注入所有用特定标签标记的服务,这在某些应用程序中是一种常见需求,因此你不必仅为此编写编译器 pass。

考虑以下 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
14
// src/HandlerCollection.php
namespace App;

use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;

class HandlerCollection
{
    public function __construct(
        // the attribute must be applied directly to the argument to autowire
        #[AutowireIterator('app.handler')]
        iterable $handlers
    ) {
    }
}

注意

当将 #[TaggedIterator]PHP 构造函数提升 一起使用时,一些 IDE 会显示错误:“属性不能应用于属性,因为它不包含 '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\AutowireIterator;

class HandlerCollection
{
    public function __construct(
        #[AutowireIterator('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\AutowireIterator;

class HandlerCollection
{
    public function __construct(
        #[AutowireIterator('app.handler', exclude: ['App\Handler\Three'], excludeSelf: false)]
        iterable $handlers
    ) {
    }
}

参见

另请参阅 标记的定位器服务

具有优先级的已标记服务

可以使用 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\AutowireIterator;

class HandlerCollection
{
    public function __construct(
        #[AutowireIterator('app.handler', defaultPriorityMethod: 'getPriority')]
        iterable $handlers
    ) {
    }
}

具有索引的已标记服务

默认情况下,标记的服务使用其服务 ID 进行索引。 你可以使用标记迭代器的两个选项(index_bydefault_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\AutowireIterator;

class HandlerCollection
{
    public function __construct(
        #[AutowireIterator('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;

        // this value is defined in the `key` option of the service
        $handlerTwo = $handlers['handler_two'];
    }
}

如果某些服务未定义在 index_by 中配置的选项/属性,Symfony 将应用此回退过程

  1. 如果服务类定义了一个名为 getDefault<CamelCase index_by value>Name 的静态方法(在本例中为 getDefaultKeyName()),则调用它并使用返回的值;
  2. 否则,回退到默认行为并使用服务 ID。

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\AutowireIterator;

class HandlerCollection
{
    public function __construct(
        #[AutowireIterator('app.handler', defaultIndexMethod: 'getIndex')]
        iterable $handlers
    ) {
    }
}

如果某些服务类未定义在 default_index_method 中配置的方法,Symfony 将回退到使用服务 ID 作为其在标记服务中的索引。

组合 index_bydefault_index_method 选项

你可以在同一标记服务集合中组合这两个选项。 Symfony 将按以下顺序处理它们

  1. 如果服务定义了在 index_by 中配置的选项/属性,则使用它;
  2. 如果服务类定义了在 default_index_method 中配置的方法,则使用它;
  3. 否则,回退到使用服务 ID 作为其在标记服务集合中的索引。

#[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
{
    // ...
}
本作品,包括代码示例,根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本