跳到內容

自動定義服務依賴性(自動裝配)

編輯此頁面

自動裝配讓您能夠以最少的配置管理容器中的服務。它會讀取您建構子(或其他方法)上的類型提示,並自動將正確的服務傳遞給每個方法。Symfony 的自動裝配旨在具有可預測性:如果不明確應該傳遞哪個依賴項,您將會看到可操作的例外狀況。

提示

由於 Symfony 的編譯容器,使用自動裝配沒有執行時的額外負擔。

自動裝配範例

假設您正在建構一個 API,以在 Twitter feed 上發布狀態,並使用 ROT13 混淆,這是一種有趣的編碼器,可在字母表中將所有字元向前移動 13 個字母。

首先建立 ROT13 轉換器類別

1
2
3
4
5
6
7
8
9
10
// src/Util/Rot13Transformer.php
namespace App\Util;

class Rot13Transformer
{
    public function transform(string $value): string
    {
        return str_rot13($value);
    }
}

現在建立一個使用此轉換器的 Twitter 用戶端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Service/TwitterClient.php
namespace App\Service;

use App\Util\Rot13Transformer;
// ...

class TwitterClient
{
    public function __construct(
        private Rot13Transformer $transformer,
    ) {
    }

    public function tweet(User $user, string $key, string $status): void
    {
        $transformedStatus = $this->transformer->transform($status);

        // ... connect to Twitter and send the encoded status
    }
}

如果您使用預設 services.yaml 配置這兩個類別都會自動註冊為服務並配置為自動裝配。這表示您可以立即使用它們,而無需任何配置。

但是,為了更好地理解自動裝配,以下範例明確配置了這兩個服務

1
2
3
4
5
6
7
8
9
10
11
12
13
# config/services.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true
    # ...

    App\Service\TwitterClient:
        # redundant thanks to _defaults, but value is overridable on each service
        autowire: true

    App\Util\Rot13Transformer:
        autowire: true

現在,您可以在控制器中立即使用 TwitterClient 服務

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/Controller/DefaultController.php
namespace App\Controller;

use App\Service\TwitterClient;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class DefaultController extends AbstractController
{
    #[Route('/tweet')]
    public function tweet(TwitterClient $twitterClient, Request $request): Response
    {
        // fetch $user, $key, $status from the POST'ed data

        $twitterClient->tweet($user, $key, $status);

        // ...
    }
}

這會自動運作!容器知道在建立 TwitterClient 服務時,將 Rot13Transformer 服務作為第一個參數傳遞。

自動裝配邏輯說明

自動裝配透過讀取 TwitterClient 中的 Rot13Transformer類型提示來運作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Service/TwitterClient.php
namespace App\Service;

// ...
use App\Util\Rot13Transformer;

class TwitterClient
{
    // ...

    public function __construct(
        private Rot13Transformer $transformer,
    ) {
    }
}

自動裝配系統會尋找 ID 與類型提示完全相符的服務:因此是 App\Util\Rot13Transformer。在本例中,該服務存在!當您配置 Rot13Transformer 服務時,您使用了其完整類別名稱作為其 ID。自動裝配不是魔法:它會尋找 ID 與類型提示相符的服務。如果您自動載入服務,則每個服務的 ID 都是其類別名稱。

如果沒有 ID 與類型完全相符的服務,則會擲出明確的例外狀況。

自動裝配是自動化配置的好方法,而 Symfony 會盡力做到可預測和盡可能清晰。

使用別名啟用自動裝配

配置自動裝配的主要方法是建立 ID 與其類別完全相符的服務。在先前的範例中,服務的 ID 為 App\Util\Rot13Transformer,這讓我們能夠自動自動裝配此類型。

這也可以使用別名來完成。假設由於某些原因,服務的 ID 反而是 app.rot13.transformer。在這種情況下,任何以類別名稱(App\Util\Rot13Transformer)作為類型提示的參數都無法再自動裝配。

沒問題!為了修正此問題,您可以透過新增服務別名來建立 ID 與類別相符的服務

1
2
3
4
5
6
7
8
9
10
11
12
13
# config/services.yaml
services:
    # ...

    # the id is not a class, so it won't be used for autowiring
    app.rot13.transformer:
        class: App\Util\Rot13Transformer
        # ...

    # but this fixes it!
    # the "app.rot13.transformer" service will be injected when
    # an App\Util\Rot13Transformer type-hint is detected
    App\Util\Rot13Transformer: '@app.rot13.transformer'

這會建立一個服務「別名」,其 ID 為 App\Util\Rot13Transformer。由於此別名,自動裝配會看到此別名,並在類型提示 Rot13Transformer 類別時使用它。

提示

核心套件使用別名來允許自動裝配服務。例如,MonologBundle 會建立 ID 為 logger 的服務。但它也會新增一個別名:指向 logger 服務的 Psr\Log\LoggerInterface。這就是為什麼可以自動裝配以 Psr\Log\LoggerInterface 作為類型提示的參數。

使用介面

您可能還會發現自己類型提示抽象概念(例如介面)而不是具體類別,因為它可以將您的依賴項替換為其他物件。

為了遵循此最佳實務,假設您決定建立 TransformerInterface

1
2
3
4
5
6
7
// src/Util/TransformerInterface.php
namespace App\Util;

interface TransformerInterface
{
    public function transform(string $value): string;
}

然後,您更新 Rot13Transformer 以實作它

1
2
3
4
5
// ...
class Rot13Transformer implements TransformerInterface
{
    // ...
}

現在您有了介面,您應該將其用作您的類型提示

1
2
3
4
5
6
7
8
9
10
class TwitterClient
{
    public function __construct(
        private TransformerInterface $transformer,
    ) {
        // ...
    }

    // ...
}

但是現在,類型提示(App\Util\TransformerInterface)不再與服務的 ID(App\Util\Rot13Transformer)相符。這表示參數無法再自動裝配。

為了修正此問題,請新增別名

1
2
3
4
5
6
7
8
9
# config/services.yaml
services:
    # ...

    App\Util\Rot13Transformer: ~

    # the App\Util\Rot13Transformer service will be injected when
    # an App\Util\TransformerInterface type-hint is detected
    App\Util\TransformerInterface: '@App\Util\Rot13Transformer'

由於 App\Util\TransformerInterface 別名,自動裝配子系統知道在處理 TransformerInterface 時,應該注入 App\Util\Rot13Transformer 服務。

提示

當使用 服務定義原型時,如果僅發現一個服務實作介面,則配置別名不是強制性的,而 Symfony 會自動建立一個別名。

提示

即使在使用聯集和交集類型時,自動裝配也夠強大,可以猜測要注入哪個服務。這表示您可以使用複雜類型來類型提示參數,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;

class DataFormatter
{
    public function __construct(
        private (NormalizerInterface&DenormalizerInterface)|SerializerInterface $transformer,
    ) {
        // ...
    }

    // ...
}

處理相同類型的多個實作

假設您建立第二個類別 - 實作 TransformerInterfaceUppercaseTransformer

1
2
3
4
5
6
7
8
9
10
// src/Util/UppercaseTransformer.php
namespace App\Util;

class UppercaseTransformer implements TransformerInterface
{
    public function transform(string $value): string
    {
        return strtoupper($value);
    }
}

如果您將其註冊為服務,您現在有兩個實作 App\Util\TransformerInterface 類型的服務。自動裝配子系統無法決定要使用哪一個。請記住,自動裝配不是魔法;它會尋找 ID 與類型提示相符的服務。因此,您需要透過從類型到正確的服務 ID 建立別名來選擇一個(請參閱自動定義服務依賴性(自動裝配))。此外,如果您想要在某些情況下使用一個實作,而在其他情況下使用另一個實作,您可以定義多個具名自動裝配別名。

例如,當類型提示 TransformerInterface 介面時,您可能想要預設使用 Rot13Transformer 實作,但在某些特定情況下使用 UppercaseTransformer 實作。若要執行此操作,您可以從 TransformerInterface 介面建立到 Rot13Transformer 的一般別名,然後從包含介面和引數名稱的特殊字串建立具名自動裝配別名,該引數名稱與您在執行注入時使用的引數名稱相符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Service/MastodonClient.php
namespace App\Service;

use App\Util\TransformerInterface;

class MastodonClient
{
    public function __construct(
        private TransformerInterface $shoutyTransformer,
    ) {
    }

    public function toot(User $user, string $key, string $status): void
    {
        $transformedStatus = $this->transformer->transform($status);

        // ... connect to Mastodon and send the transformed status
    }
}
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
# config/services.yaml
services:
    # ...

    App\Util\Rot13Transformer: ~
    App\Util\UppercaseTransformer: ~

    # the App\Util\UppercaseTransformer service will be
    # injected when an App\Util\TransformerInterface
    # type-hint for a $shoutyTransformer argument is detected
    App\Util\TransformerInterface $shoutyTransformer: '@App\Util\UppercaseTransformer'

    # If the argument used for injection does not match, but the
    # type-hint still matches, the App\Util\Rot13Transformer
    # service will be injected.
    App\Util\TransformerInterface: '@App\Util\Rot13Transformer'

    App\Service\TwitterClient:
        # the Rot13Transformer will be passed as the $transformer argument
        autowire: true

        # If you wanted to choose the non-default service and do not
        # want to use a named autowiring alias, wire it manually:
        # arguments:
        #     $transformer: '@App\Util\UppercaseTransformer'
        # ...

由於 App\Util\TransformerInterface 別名,任何以該介面作為類型提示的參數都將傳遞 App\Util\Rot13Transformer 服務。如果參數命名為 $shoutyTransformer,則將改用 App\Util\UppercaseTransformer。但是,您也可以透過在 arguments 鍵下指定參數來手動連接任何其他服務。

另一個選項是使用 #[Target] 屬性。透過將此屬性新增至您想要自動裝配的參數,您可以透過傳遞具名別名中使用的參數名稱來指定要注入哪個服務。透過這種方式,您可以擁有實作相同介面的多個服務,並使參數名稱與任何實作名稱分開(如上例所示)。此外,如果您在目標名稱中輸入任何錯字,您將收到例外狀況。

警告

#[Target] 屬性僅接受具名別名中使用的參數名稱;它接受服務 ID 或服務別名。

您可以執行 debug:autowiring 命令來取得具名自動裝配別名的清單

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ php bin/console debug:autowiring LoggerInterface

Autowirable Types
=================

 The following classes & interfaces can be used as type-hints when autowiring:
 (only showing classes/interfaces matching LoggerInterface)

 Describes a logger instance.
 Psr\Log\LoggerInterface - alias:monolog.logger
 Psr\Log\LoggerInterface $assetMapperLogger - target:asset_mapperLogger - alias:monolog.logger.asset_mapper
 Psr\Log\LoggerInterface $cacheLogger - alias:monolog.logger.cache
 Psr\Log\LoggerInterface $httpClientLogger - target:http_clientLogger - alias:monolog.logger.http_client
 Psr\Log\LoggerInterface $mailerLogger - alias:monolog.logger.mailer

 [...]

假設您想要注入 App\Util\UppercaseTransformer 服務。您將透過傳遞 $shoutyTransformer 參數的名稱來使用 #[Target] 屬性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Service/MastodonClient.php
namespace App\Service;

use App\Util\TransformerInterface;
use Symfony\Component\DependencyInjection\Attribute\Target;

class MastodonClient
{
    public function __construct(
        #[Target('shoutyTransformer')]
        private TransformerInterface $transformer,
    ) {
    }
}

提示

由於 #[Target] 屬性會將傳遞給它的字串正規化為其 camelCased 形式,因此名稱變體(例如 shouty.transformer)也有效。

注意

當使用 #[Target] 時,某些 IDE 會顯示錯誤,如先前的範例所示:「屬性無法套用至屬性,因為它不包含 'Attribute::TARGET_PROPERTY' 旗標」。原因是,由於 PHP 建構子提升,此建構子參數既是參數又是類別屬性。您可以安全地忽略此錯誤訊息。

修正無法自動裝配的參數

自動裝配僅在您的參數是物件時才有效。但是,如果您有純量參數(例如字串),則無法自動裝配:Symfony 將擲出明確的例外狀況。

為了修正此問題,您可以在服務配置中手動連接有問題的參數。您僅連接困難的參數,Symfony 會處理其餘部分。

您也可以使用 #[Autowire] 參數屬性來指示自動裝配邏輯關於這些參數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Service/MessageGenerator.php
namespace App\Service;

use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

class MessageGenerator
{
    public function __construct(
        #[Autowire(service: 'monolog.logger.request')]
        private LoggerInterface $logger,
    ) {
        // ...
    }
}

#[Autowire] 屬性也可以用於參數複雜的運算式,甚至環境變數包括環境變數處理器

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
// src/Service/MessageGenerator.php
namespace App\Service;

use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

class MessageGenerator
{
    public function __construct(
        // use the %...% syntax for parameters
        #[Autowire('%kernel.project_dir%/data')]
        string $dataDir,

        // or use argument "param"
        #[Autowire(param: 'kernel.debug')]
        bool $debugMode,

        // expressions
        #[Autowire(expression: 'service("App\\\Mail\\\MailerConfiguration").getMailerMethod()')]
        string $mailerMethod,

        // environment variables
        #[Autowire(env: 'SOME_ENV_VAR')]
        string $senderName,

        // environment variables with processors
        #[Autowire(env: 'bool:SOME_BOOL_ENV_VAR')]
        bool $allowAttachments,
    ) {
    }
    // ...
}

使用自動裝配產生閉包

服務閉包是傳回服務的匿名函式。當您處理延遲載入時,此類型的實例化非常方便。它也適用於非共用服務依賴項。

使用 AutowireServiceClosure 屬性可以自動建立封裝服務實例化的閉包

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
36
37
38
39
40
// src/Service/Remote/MessageFormatter.php
namespace App\Service\Remote;

use Symfony\Component\DependencyInjection\Attribute\AsAlias;

#[AsAlias('third_party.remote_message_formatter')]
class MessageFormatter
{
    public function __construct()
    {
        // ...
    }

    public function format(string $message): string
    {
        // ...
    }
}

// src/Service/MessageGenerator.php
namespace App\Service;

use App\Service\Remote\MessageFormatter;
use Symfony\Component\DependencyInjection\Attribute\AutowireServiceClosure;

class MessageGenerator
{
    public function __construct(
        #[AutowireServiceClosure('third_party.remote_message_formatter')]
        private \Closure $messageFormatterResolver,
    ) {
    }

    public function generate(string $message): void
    {
        $formattedMessage = ($this->messageFormatterResolver)()->format($message);

        // ...
    }
}

服務接受具有特定簽名的閉包是很常見的。在這種情況下,您可以使用 AutowireCallable 屬性來產生具有與服務特定方法相同簽名的閉包。當呼叫此閉包時,它會將其所有參數傳遞至基礎服務函式。如果需要多次呼叫閉包,則服務實例會重複用於重複呼叫。與服務閉包不同,這不會建立非共用服務的額外實例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Service/MessageGenerator.php
namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;

class MessageGenerator
{
    public function __construct(
        #[AutowireCallable(service: 'third_party.remote_message_formatter', method: 'format')]
        private \Closure $formatCallable,
    ) {
    }

    public function generate(string $message): void
    {
        $formattedMessage = ($this->formatCallable)($message);

        // ...
    }
}

最後,您可以將 lazy: true 選項傳遞至 AutowireCallable 屬性。透過這樣做,callable 將自動為延遲載入,這表示封裝的服務將在閉包的第一次呼叫時實例化。

AutowireMethodOf 屬性提供了一種更簡單的方法來指定服務方法的名稱,方法是使用屬性名稱作為方法名稱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Service/MessageGenerator.php
namespace App\Service;

use Symfony\Component\DependencyInjection\Attribute\AutowireMethodOf;

class MessageGenerator
{
    public function __construct(
        #[AutowireMethodOf('third_party.remote_message_formatter')]
        private \Closure $format,
    ) {
    }

    public function generate(string $message): void
    {
        $formattedMessage = ($this->format)($message);

        // ...
    }
}

7.1

AutowireMethodOf 屬性是在 Symfony 7.1 中引入的。

自動裝配其他方法(例如 Setter 和公有類型屬性)

當為服務啟用自動裝配時,您也可以配置容器以在類別實例化時呼叫類別上的方法。例如,假設您想要注入 logger 服務,並決定使用 Setter 注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/Util/Rot13Transformer.php
namespace App\Util;

use Symfony\Contracts\Service\Attribute\Required;

class Rot13Transformer
{
    private LoggerInterface $logger;

    #[Required]
    public function setLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;
    }

    public function transform($value): string
    {
        $this->logger->info('Transforming '.$value);
        // ...
    }
}

自動裝配將自動呼叫上方具有 #[Required] 屬性的任何方法,自動裝配每個參數。如果您需要手動連接方法的某些參數,您可以隨時明確地配置方法呼叫

儘管屬性注入有一些缺點,但搭配 #[Required] 的自動裝配也可以應用於公有類型屬性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace App\Util;

use Symfony\Contracts\Service\Attribute\Required;

class Rot13Transformer
{
    #[Required]
    public LoggerInterface $logger;

    public function transform($value): void
    {
        $this->logger->info('Transforming '.$value);
        // ...
    }
}

自動裝配控制器動作方法

如果您使用的是 Symfony Framework,您也可以自動裝配控制器動作方法的參數。這是為了方便起見而存在的自動裝配的特殊情況。請參閱控制器以取得更多詳細資訊。

效能後果

由於 Symfony 的編譯容器,使用自動裝配沒有效能損失。但是,在 dev 環境中,由於容器可能會在您修改類別時更頻繁地重建,因此會產生少許效能損失。如果重建容器速度很慢(在非常大型的專案上可能發生),您可能無法使用自動裝配。

公有和可重複使用的套件

公有套件應明確配置其服務,而不應依賴自動裝配。自動裝配取決於容器中可用的服務,而套件無法控制它們包含在其中的應用程式的服務容器。當您在公司內部建構可重複使用的套件時,可以使用自動裝配,因為您可以完全控制所有程式碼。

本作品,包括程式碼範例,均根據 Creative Commons BY-SA 3.0 授權條款授權。
目錄
    版本