自動定義服務依賴性(自動裝配)
自動裝配讓您能夠以最少的配置管理容器中的服務。它會讀取您建構子(或其他方法)上的類型提示,並自動將正確的服務傳遞給每個方法。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,
) {
// ...
}
// ...
}
處理相同類型的多個實作
假設您建立第二個類別 - 實作 TransformerInterface
的 UppercaseTransformer
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
環境中,由於容器可能會在您修改類別時更頻繁地重建,因此會產生少許效能損失。如果重建容器速度很慢(在非常大型的專案上可能發生),您可能無法使用自動裝配。
公有和可重複使用的套件
公有套件應明確配置其服務,而不應依賴自動裝配。自動裝配取決於容器中可用的服務,而套件無法控制它們包含在其中的應用程式的服務容器。當您在公司內部建構可重複使用的套件時,可以使用自動裝配,因為您可以完全控制所有程式碼。