跳到主要內容

如何使用服務標籤

編輯此頁面

服務標籤是一種告知 Symfony 或其他第三方套件,您的服務應該以某種特殊方式註冊的方法。請看以下範例

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 選項

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 應用程式中,在您的 kernel 類別中呼叫此方法

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() 方法中呼叫此方法bundle extension class

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 extension或從您的 kernel 將編譯器 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' }

請注意,您已將通用別名鍵新增至標籤。若要實際使用它,請更新編譯器

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
    ) {
    }
}

注意

某些 IDE 在將 #[TaggedIterator]PHP 建構子提升一起使用時,會顯示錯誤:'屬性無法套用至屬性,因為它不包含 '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
{
    // ...
}
這份作品,包括程式碼範例,皆以創用 CC BY-SA 3.0 授權條款釋出。
目錄
    版本