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_PROPERTY
和 Attribute::TARGET_PARAMETER
;然后在你的 registerAttributeForAutoconfiguration() 可调用对象中支持 ReflectionProperty
和 ReflectionParameter
。
创建自定义标签
标签本身实际上不会以任何方式改变你的服务的功能。 但是,如果你选择这样做,你可以向容器构建器请求使用某些特定标签标记的所有服务的列表。 这在编译器 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_by
和 default_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 将应用此回退过程
- 如果服务类定义了一个名为
getDefault<CamelCase index_by value>Name
的静态方法(在本例中为getDefaultKeyName()
),则调用它并使用返回的值; - 否则,回退到默认行为并使用服务 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_by
和 default_index_method
选项
你可以在同一标记服务集合中组合这两个选项。 Symfony 将按以下顺序处理它们
- 如果服务定义了在
index_by
中配置的选项/属性,则使用它; - 如果服务类定义了在
default_index_method
中配置的方法,则使用它; - 否则,回退到使用服务 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
{
// ...
}