跳到内容

自动定义服务依赖关系 (自动装配)

编辑此页

自动装配允许你以最少的配置在容器中管理服务。它读取你的构造函数(或其他方法)上的类型提示,并自动将正确的服务传递给每个方法。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 环境中存在轻微的性能损失,因为当你修改类时,容器可能会更频繁地重建。如果重建容器很慢(在非常大的项目中可能),你可能无法使用自动装配。

公共和可重用的扩展包

公共扩展包应显式配置其服务,而不是依赖自动装配。自动装配取决于容器中可用的服务,而扩展包无法控制它们包含在其中的应用程序的服务容器。当在你的公司内构建可重用扩展包时,你可以使用自动装配,因为你对所有代码拥有完全控制权。

这项工作,包括代码示例,均根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本