自动定义服务依赖关系 (自动装配)
自动装配允许你以最少的配置在容器中管理服务。它读取你的构造函数(或其他方法)上的类型提示,并自动将正确的服务传递给每个方法。Symfony 的自动装配被设计为可预测的:如果无法明确确定应该传递哪个依赖项,你将看到一个可操作的异常。
提示
感谢 Symfony 的编译容器,使用自动装配没有运行时开销。
自动装配的示例
假设你正在构建一个 API,用于在 Twitter feed 上发布状态,使用 ROT13 进行混淆,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
的服务。但它也添加了一个别名:Psr\Log\LoggerInterface
,它指向 logger
服务。这就是为什么类型提示为 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,
) {
// ...
}
// ...
}
处理同一类型的多个实现
假设你创建了第二个类 - UppercaseTransformer
,它实现了 TransformerInterface
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]
属性将其传递的字符串规范化为驼峰形式,因此名称变体(例如 shouty.transformer
)也有效。
注意
某些 IDE 在使用 #[Target]
时会显示错误,如前面的示例所示:“Attribute cannot be applied to a property because it does not contain the 'Attribute::TARGET_PROPERTY' flag”。原因是由于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 属性。这样做,可调用对象将自动变为延迟加载,这意味着封装的服务将仅在闭包的第一次调用时实例化。
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 框架,你还可以自动装配控制器操作方法的参数。这是自动装配的一个特殊情况,它的存在是为了方便。有关更多详细信息,请参阅控制器。
性能影响
感谢 Symfony 的编译容器,使用自动装配没有性能损失。但是,在 dev
环境中存在轻微的性能损失,因为当你修改类时,容器可能会更频繁地重建。如果重建容器很慢(在非常大的项目中可能),你可能无法使用自动装配。
公共和可重用的扩展包
公共扩展包应显式配置其服务,而不是依赖自动装配。自动装配取决于容器中可用的服务,而扩展包无法控制它们包含在其中的应用程序的服务容器。当在你的公司内构建可重用扩展包时,你可以使用自动装配,因为你对所有代码拥有完全控制权。