跳到内容

服务容器

编辑此页

视频教程

您更喜欢视频教程吗?查看 Symfony 基础知识视频教程系列

你的应用程序充满了有用的对象:一个 “Mailer” 对象可以帮助你发送邮件,而另一个对象可以帮助你将内容保存到数据库。几乎所有你的应用 “做” 的事情实际上都是由这些对象之一完成的。并且每次你安装一个新的程序包,你都可以访问更多的对象!

在 Symfony 中,这些有用的对象被称为 服务 (services),每个服务都存在于一个非常特殊的被称为 服务容器 (service container) 的对象中。容器允许你集中管理对象的构建方式。它使你的生活更轻松,促进了强大的架构,并且速度超快!

获取和使用服务

当你启动一个 Symfony 应用时,你的容器已经包含了许多服务。这些服务就像工具:等待你来利用它们。在你的控制器中,你可以通过类型提示一个参数为服务的类或接口名称,来 “请求” 容器中的服务。想要 记录日志 吗?没问题:

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

use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class ProductController extends AbstractController
{
    #[Route('/products')]
    public function list(LoggerInterface $logger): Response
    {
        $logger->info('Look, I just used a service!');

        // ...
    }
}

还有哪些其他服务可用?通过运行以下命令来查找:

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

  # this is just a *small* sample of the output...

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

   The following classes & interfaces can be used as type-hints when autowiring:

   Describes a logger instance.
   Psr\Log\LoggerInterface - alias:logger

   Request stack that controls the lifecycle of requests.
   Symfony\Component\HttpFoundation\RequestStack - alias:request_stack

   RouterInterface is the interface that all Router classes must implement.
   Symfony\Component\Routing\RouterInterface - alias:router.default

   [...]

当你在你的控制器方法或你的 自己的服务 中使用这些类型提示时,Symfony 将自动传递给你匹配该类型的服务对象。

在整个文档中,你将看到如何使用容器中存在的许多不同的服务。

提示

实际上,容器中还有更多的服务,每个服务在容器中都有一个唯一的 ID,例如 request_stackrouter.default。要获取完整列表,你可以运行 php bin/console debug:container。但是大多数时候,你不需要担心这一点。请参阅 如何选择特定的服务。请参阅 如何调试服务容器并列出服务

在容器中创建/配置服务

你也可以将你自己的代码组织成服务。例如,假设你需要向你的用户显示一个随机的、令人愉悦的消息。如果你将此代码放在你的控制器中,则无法重用。相反,你决定创建一个新类:

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

class MessageGenerator
{
    public function getHappyMessage(): string
    {
        $messages = [
            'You did it! You updated the system! Amazing!',
            'That was one of the coolest updates I\'ve seen all day!',
            'Great work! Keep going!',
        ];

        $index = array_rand($messages);

        return $messages[$index];
    }
}

恭喜!你已经创建了你的第一个服务类!你可以立即在你的控制器中使用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/Controller/ProductController.php
use App\Service\MessageGenerator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

class ProductController extends AbstractController
{
    #[Route('/products/new')]
    public function new(MessageGenerator $messageGenerator): Response
    {
        // thanks to the type-hint, the container will instantiate a
        // new MessageGenerator and pass it to you!
        // ...

        $message = $messageGenerator->getHappyMessage();
        $this->addFlash('success', $message);
        // ...
    }
}

当你请求 MessageGenerator 服务时,容器会构造一个新的 MessageGenerator 对象并返回它(见下面的侧边栏)。但是,如果你从不请求该服务,则永远不会构造它:节省内存和速度。作为奖励,MessageGenerator 服务只创建一次:每次你请求它时,都会返回相同的实例。

该文档假设你正在使用以下服务配置,这是新项目的默认配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# config/services.yaml
services:
    # default configuration for services in *this* file
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.

    # makes classes in src/ available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    App\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'

    # order is important in this file because service definitions
    # always *replace* previous ones; add your own service configuration below

    # ...

提示

resourceexclude 选项的值可以是任何有效的 glob 模式exclude 选项的值也可以是 glob 模式的数组。

感谢此配置,你可以自动使用 src/ 目录中的任何类作为服务,而无需手动配置它。稍后,你将学习如何使用 resource 一次性导入多个服务

如果你更喜欢手动连接你的服务,你可以使用显式配置

将服务限制在特定的 Symfony 环境

你可以使用 #[When] 属性仅在某些环境中将类注册为服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use Symfony\Component\DependencyInjection\Attribute\When;

// SomeClass is only registered in the "dev" environment

#[When(env: 'dev')]
class SomeClass
{
    // ...
}

// you can also apply more than one When attribute to the same class

#[When(env: 'dev')]
#[When(env: 'test')]
class AnotherClass
{
    // ...
}

如果你想排除某个服务在特定环境中注册,你可以使用 #[WhenNot] 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use Symfony\Component\DependencyInjection\Attribute\WhenNot;

// SomeClass is registered in all environments except "dev"

#[WhenNot(env: 'dev')]
class SomeClass
{
    // ...
}

// you can apply more than one WhenNot attribute to the same class

#[WhenNot(env: 'dev')]
#[WhenNot(env: 'test')]
class AnotherClass
{
    // ...
}

7.2

#[WhenNot] 属性在 Symfony 7.2 中引入。

将服务/配置注入到服务中

如果你需要从 MessageGenerator 内部访问 logger 服务怎么办?没问题!创建一个带有 $logger 参数的 __construct() 方法,该参数具有 LoggerInterface 类型提示。将其设置在一个新的 $logger 属性上,并在稍后使用它:

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

use Psr\Log\LoggerInterface;

class MessageGenerator
{
    public function __construct(
        private LoggerInterface $logger,
    ) {
    }

    public function getHappyMessage(): string
    {
        $this->logger->info('About to find a happy message!');
        // ...
    }
}

就是这样!容器会自动知道在实例化 MessageGenerator 时传递 logger 服务。它是如何知道做到这一点的?自动装配 (Autowiring)。关键是在你的 __construct() 方法中的 LoggerInterface 类型提示以及 services.yaml 中的 autowire: true 配置。当你类型提示一个参数时,容器将自动找到匹配的服务。如果找不到,你将看到一个清晰的异常,并提供有用的建议。

顺便说一句,这种将依赖项添加到你的 __construct() 方法的方法称为依赖注入 (dependency injection)

你应该如何知道为类型提示使用 LoggerInterface?你可以阅读你正在使用的任何功能的文档,或者通过运行以下命令获取可自动装配的类型提示列表:

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

  # this is just a *small* sample of the output...

  Describes a logger instance.
  Psr\Log\LoggerInterface - alias:monolog.logger

  Request stack that controls the lifecycle of requests.
  Symfony\Component\HttpFoundation\RequestStack - alias:request_stack

  RouterInterface is the interface that all Router classes must implement.
  Symfony\Component\Routing\RouterInterface - alias:router.default

  [...]

处理多个服务

假设你还想在每次站点更新时通过电子邮件通知站点管理员。为此,你创建一个新类:

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

use App\Service\MessageGenerator;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;

class SiteUpdateManager
{
    public function __construct(
        private MessageGenerator $messageGenerator,
        private MailerInterface $mailer,
    ) {
    }

    public function notifyOfSiteUpdate(): bool
    {
        $happyMessage = $this->messageGenerator->getHappyMessage();

        $email = (new Email())
            ->from('[email protected]')
            ->to('[email protected]')
            ->subject('Site update just happened!')
            ->text('Someone just updated the site. We told them: '.$happyMessage);

        $this->mailer->send($email);

        // ...

        return true;
    }
}

这需要 MessageGenerator Mailer 服务。没问题,我们通过类型提示它们的类和接口名称来请求它们!现在,这个新服务已准备好使用。例如,在控制器中,你可以类型提示新的 SiteUpdateManager 类并使用它:

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

use App\Service\SiteUpdateManager;
// ...

class SiteController extends AbstractController
{
    public function new(SiteUpdateManager $siteUpdateManager): Response
    {
        // ...

        if ($siteUpdateManager->notifyOfSiteUpdate()) {
            $this->addFlash('success', 'Notification mail was sent successfully.');
        }

        // ...
    }
}

感谢自动装配和你 __construct() 中的类型提示,容器会创建 SiteUpdateManager 对象并传递给它正确的参数。在大多数情况下,这都能完美地工作。

手动连接参数

但是,在少数情况下,服务的参数无法自动装配。例如,假设你想使管理员电子邮件可配置:

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
// src/Service/SiteUpdateManager.php
  // ...

  class SiteUpdateManager
  {
      // ...

      public function __construct(
          private MessageGenerator $messageGenerator,
          private MailerInterface $mailer,
+         private string $adminEmail
      ) {
      }

      public function notifyOfSiteUpdate(): bool
      {
          // ...

          $email = (new Email())
              // ...
-            ->to('[email protected]')
+            ->to($this->adminEmail)
              // ...
          ;
          // ...
      }
  }

如果你进行此更改并刷新,你将看到一个错误:

无法自动装配服务 "App\Service\SiteUpdateManager":方法 "__construct()" 的参数 "$adminEmail" 必须具有类型提示或显式给定值。

这很有道理!容器无法知道你想在此处传递什么值。没问题!在你的配置中,你可以显式设置此参数:

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

    # same as before
    App\:
        resource: '../src/'
        exclude: '../src/{DependencyInjection,Entity,Kernel.php}'

    # explicitly configure the service
    App\Service\SiteUpdateManager:
        arguments:
            $adminEmail: '[email protected]'

因此,容器将在创建 SiteUpdateManager 服务时将 [email protected] 传递给 __construct$adminEmail 参数。其他参数仍将自动装配。

但是,这不是很脆弱吗?幸运的是,不是!如果你将 $adminEmail 参数重命名为其他名称 - 例如 $mainEmail - 则在重新加载下一页时,你将获得一个清晰的异常(即使该页面未使用此服务)。

服务参数

除了保存服务对象外,容器还保存配置,称为 参数 (parameters)。关于 Symfony 配置的主要文章详细解释了 配置参数,并显示了它们的所有类型(字符串、布尔值、数组、二进制和 PHP 常量参数)。

但是,还有另一种与服务相关的参数类型。在 YAML 配置中,任何以 @ 开头的字符串都被视为服务的 ID,而不是常规字符串。在 XML 配置中,为参数使用 type="service" 类型,在 PHP 配置中使用 service() 函数:

1
2
3
4
5
6
7
8
9
10
11
# config/services.yaml
services:
    App\Service\MessageGenerator:
        arguments:
            # this is not a string, but a reference to a service called 'logger'
            - '@logger'

            # if the value of a string argument starts with '@', you need to escape
            # it by adding another '@' so Symfony doesn't consider it a service
            # the following example would be parsed as the string '@securepassword'
            # - '@@securepassword'

使用容器的参数访问器方法可以轻松处理容器参数:

1
2
3
4
5
6
7
8
// checks if a parameter is defined (parameter names are case-sensitive)
$container->hasParameter('mailer.transport');

// gets value of a parameter
$container->getParameter('mailer.transport');

// adds a new parameter
$container->setParameter('mailer.transport', 'sendmail');

警告

使用的 . 表示法是 Symfony 约定,使参数更易于阅读。参数是扁平的键值元素,它们不能组织成嵌套数组。

注意

你只能在容器编译之前设置参数,而不能在运行时设置。要了解有关编译容器的更多信息,请参阅 编译容器

选择特定的服务

先前创建的 MessageGenerator 服务需要一个 LoggerInterface 参数:

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

use Psr\Log\LoggerInterface;

class MessageGenerator
{
    public function __construct(
        private LoggerInterface $logger,
    ) {
    }
    // ...
}

但是,容器中有多个服务实现了 LoggerInterface,例如 loggermonolog.logger.requestmonolog.logger.php 等。容器如何知道使用哪一个?

在这些情况下,容器通常被配置为自动选择其中一个服务 - 在这种情况下为 logger(在 自动定义服务依赖项 (自动装配) 中阅读更多关于原因的信息)。但是,你可以控制这一点并传入不同的记录器:

1
2
3
4
5
6
7
8
9
10
11
# config/services.yaml
services:
    # ... same code as before

    # explicitly configure the service
    App\Service\MessageGenerator:
        arguments:
            # the '@' symbol is important: that's what tells the container
            # you want to pass the *service* whose id is 'monolog.logger.request',
            # and not just the *string* 'monolog.logger.request'
            $logger: '@monolog.logger.request'

这告诉容器,__construct$logger 参数应使用 ID 为 monolog.logger.request 的服务。

要获得可以与自动装配一起使用的可能的记录器服务列表,请运行:

1
$ php bin/console debug:autowiring logger

要获得容器中所有可能的服务的完整列表,请运行:

1
$ php bin/console debug:container

移除服务

如果需要,可以从服务容器中移除服务。例如,这对于使服务在某些 配置环境 中不可用很有用(例如在 test 环境中):

1
2
3
4
5
6
7
8
9
10
// config/services_test.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use App\RemovedService;

return function(ContainerConfigurator $containerConfigurator) {
    $services = $containerConfigurator->services();

    $services->remove(RemovedService::class);
};

现在,容器在 test 环境中将不包含 App\RemovedService

注入闭包作为参数

可以将可调用对象作为服务的参数注入。让我们为我们的 MessageGenerator 构造函数添加一个参数:

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

use Psr\Log\LoggerInterface;

class MessageGenerator
{
    private string $messageHash;

    public function __construct(
        private LoggerInterface $logger,
        callable $generateMessageHash,
    ) {
        $this->messageHash = $generateMessageHash();
    }
    // ...
}

现在,我们将添加一个新的可调用服务来生成消息哈希:

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

class MessageHashGenerator
{
    public function __invoke(): string
    {
        // Compute and return a message hash
    }
}

我们的配置如下所示:

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

    # explicitly configure the service
    App\Service\MessageGenerator:
        arguments:
            $logger: '@monolog.logger.request'
            $generateMessageHash: !closure '@App\Hash\MessageHashGenerator'

另请参阅

可以使用 自动装配 及其专用属性来注入闭包。

按名称或类型绑定参数

你还可以使用 bind 关键字按名称或类型绑定特定参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# config/services.yaml
services:
    _defaults:
        bind:
            # pass this value to any $adminEmail argument for any service
            # that's defined in this file (including controller arguments)
            $adminEmail: '[email protected]'

            # pass this service to any $requestLogger argument for any
            # service that's defined in this file
            $requestLogger: '@monolog.logger.request'

            # pass this service for any LoggerInterface type-hint for any
            # service that's defined in this file
            Psr\Log\LoggerInterface: '@monolog.logger.request'

            # optionally you can define both the name and type of the argument to match
            string $adminEmail: '[email protected]'
            Psr\Log\LoggerInterface $requestLogger: '@monolog.logger.request'
            iterable $rules: !tagged_iterator app.foo.rule

    # ...

通过将 bind 键放在 _defaults 下,你可以为在此文件中定义的任何服务指定任何参数的值!你可以按名称(例如 $adminEmail)、按类型(例如 Psr\Log\LoggerInterface)或两者(例如 Psr\Log\LoggerInterface $requestLogger)绑定参数。

bind 配置也可以应用于特定服务或在 一次加载多个服务 时应用。

抽象服务参数

有时,某些服务参数的值无法在配置文件中定义,因为它们是在运行时使用 编译器传递程序包扩展 计算的。

在这些情况下,你可以使用 abstract 参数类型来至少定义参数的名称和有关其用途的简短描述:

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

    App\Service\MyService:
        arguments:
            $rootNamespace: !abstract 'should be defined by Pass'

    # ...

如果你在运行时不替换抽象参数的值,则会抛出 RuntimeException,并显示类似 Argument "$rootNamespace" of service "App\Service\MyService" is abstract: should be defined by Pass. 的消息。

autowire 选项

上面,services.yaml 文件在 _defaults 部分中具有 autowire: true,以便它应用于该文件中定义的所有服务。使用此设置,你可以在服务的 __construct() 方法中类型提示参数,并且容器将自动传递给你正确的参数。整个条目都是围绕自动装配编写的。

有关自动装配的更多详细信息,请查看 自动定义服务依赖项 (自动装配)

autoconfigure 选项

上面,services.yaml 文件在 _defaults 部分中具有 autoconfigure: true,以便它应用于该文件中定义的所有服务。使用此设置,容器将根据你的服务的自动将某些配置应用于你的服务。这主要用于自动标记 (auto-tag) 你的服务。

例如,要创建 Twig 扩展,你需要创建一个类,将其注册为服务,并使用 twig.extension 标记 它。

但是,使用 autoconfigure: true,你不需要标签。实际上,如果你正在使用 默认的 services.yaml 配置,你什么都不需要做:服务将自动加载。然后,autoconfigure为你添加 twig.extension 标签,因为你的类实现了 Twig\Extension\ExtensionInterface。并且由于 autowire,你甚至可以添加构造函数参数而无需任何配置。

自动配置也适用于属性。某些属性(如 AsMessageHandlerAsEventListenerAsCommand)已注册用于自动配置。任何使用这些属性的类都将应用标签。

Linting 服务定义

lint:container 命令执行额外的检查,以确保容器已正确配置。在将应用程序部署到生产环境之前(例如,在你的持续集成服务器中)运行此命令很有用:

1
2
3
4
5
$ php bin/console lint:container

# optionally, you can force the resolution of environment variables;
# the command will fail if any of those environment variables are missing
$ php bin/console lint:container --resolve-env-vars

7.2

--resolve-env-vars 选项在 Symfony 7.2 中引入。

每当编译容器时执行这些检查可能会损害性能。这就是为什么它们在称为 CheckTypeDeclarationsPassCheckAliasValidityPass编译器传递 中实现的原因,这些编译器传递默认情况下禁用,仅在执行 lint:container 命令时启用。如果你不介意性能损失,则可以在你的应用程序中启用这些编译器传递。

7.1

CheckAliasValidityPass 编译器传递在 Symfony 7.1 中引入。

公共与私有服务

默认情况下,每个定义的服务都是私有的。当服务是私有时,你无法使用 $container->get() 从容器直接访问它。作为最佳实践,你应仅创建私有服务,并且应使用依赖注入而不是使用 $container->get() 来获取服务。

如果你需要延迟获取服务,则应考虑使用 服务定位器,而不是使用公共服务。

但是,如果你确实需要使服务公开,请覆盖 public 设置:

1
2
3
4
5
6
7
# config/services.yaml
services:
    # ... same code as before

    # explicitly configure the service
    App\Service\PublicService:
        public: true

也可以借助 #[Autoconfigure] 属性将服务定义为公共的。此属性必须直接用于你要配置的服务的类上:

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

use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;

#[Autoconfigure(public: true)]
class PublicService
{
    // ...
}

使用 resource 一次性导入多个服务

你已经看到可以使用 resource 键一次性导入多个服务。例如,默认的 Symfony 配置包含以下内容:

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

    # makes classes in src/ available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    App\:
        resource: '../src/'
        exclude: '../src/{DependencyInjection,Entity,Kernel.php}'

提示

resourceexclude 选项的值可以是任何有效的 glob 模式。如果你只想排除少数服务,则可以直接在你的类上使用 Exclude 属性来排除它。

这可以用于快速使许多类作为服务可用,并应用一些默认配置。每个服务的 id 是其完全限定的类名。你可以通过在其下方使用其 ID(类名)来覆盖任何导入的服务(例如,请参阅 如何手动连接参数)。如果你覆盖服务,则任何选项(例如 public)都不会从导入中继承(但是被覆盖的服务仍然_defaults 继承)。

您也可以 exclude 某些路径。这是可选的,但会在 dev 环境中略微提高性能:排除的路径不会被跟踪,因此修改它们不会导致容器重建。

注意

等等,这是否意味着 src/ 中的每个类都被注册为服务?甚至是模型类?实际上,并非如此。只要您将导入的服务保持为 私有src/显式用作服务的所有类都会自动从最终容器中移除。实际上,导入意味着所有类“都可以用作服务”,而无需手动配置。

使用相同命名空间的多个服务定义

如果您使用 YAML 配置格式定义服务,PHP 命名空间将用作每个配置的键,因此您无法为同一命名空间下的类定义不同的服务配置

1
2
3
4
5
# config/services.yaml
services:
    App\Domain\:
        resource: '../src/Domain/*'
        # ...

为了拥有多个定义,请添加 namespace 选项,并使用任何唯一字符串作为每个服务配置的键

1
2
3
4
5
6
7
8
9
10
11
# config/services.yaml
services:
    command_handlers:
        namespace: App\Domain\
        resource: '../src/Domain/*/CommandHandler'
        tags: [command_handler]

    event_subscribers:
        namespace: App\Domain\
        resource: '../src/Domain/*/EventSubscriber'
        tags: [event_subscriber]

显式配置服务和参数

自动加载服务自动装配 是可选的。即使您使用它们,在某些情况下您可能也想手动装配服务。例如,假设您想为 SiteUpdateManager 类注册 2 个服务 - 每个服务具有不同的管理员电子邮件。在这种情况下,每个服务都需要有一个唯一的服务 ID

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:
    # ...

    # this is the service's id
    site_update_manager.superadmin:
        class: App\Service\SiteUpdateManager
        # you CAN still use autowiring: we just want to show what it looks like without
        autowire: false
        # manually wire all arguments
        arguments:
            - '@App\Service\MessageGenerator'
            - '@mailer'
            - '[email protected]'

    site_update_manager.normal_users:
        class: App\Service\SiteUpdateManager
        autowire: false
        arguments:
            - '@App\Service\MessageGenerator'
            - '@mailer'
            - '[email protected]'

    # Create an alias, so that - by default - if you type-hint SiteUpdateManager,
    # the site_update_manager.superadmin will be used
    App\Service\SiteUpdateManager: '@site_update_manager.superadmin'

在这种情况下,注册了 两个 服务:site_update_manager.superadminsite_update_manager.normal_users。由于别名的存在,如果您类型提示 SiteUpdateManager,则第一个服务 (site_update_manager.superadmin) 将被传递。

如果您想传递第二个服务,您需要 手动装配服务 或创建一个命名的 自动装配别名

警告

如果您没有创建别名并且 从 src/ 加载所有服务,那么将创建 三个 服务(自动服务 + 您的两个服务),并且当您类型提示 SiteUpdateManager 时,默认情况下将传递自动加载的服务。这就是为什么创建别名是一个好主意。

当使用 PHP 闭包来配置服务时,可以通过向闭包添加名为 $env 的字符串参数来自动注入当前环境值

1
2
3
4
5
6
7
// config/packages/my_config.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

return function(ContainerConfigurator $containerConfigurator, string $env): void {
    // `$env` is automatically filled in, so you can configure your
    // services depending on which environment you're on
};

为函数式接口生成适配器

函数式接口是只有一个方法的接口。它们在概念上与闭包非常相似,只是它们唯一的​​方法有一个名称。此外,它们可以用作代码中的类型提示。

AutowireCallable 属性可用于为函数式接口生成适配器。假设您有以下函数式接口

1
2
3
4
5
6
7
// src/Service/MessageFormatterInterface.php
namespace App\Service;

interface MessageFormatterInterface
{
    public function format(string $message, array $parameters): string;
}

您还有一个服务定义了许多方法,其中一种方法与先前接口的 format() 方法相同

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

class MessageUtils
{
    // other methods...

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

由于 #[AutowireCallable] 属性,您现在可以将此 MessageUtils 服务作为函数式接口实现注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
namespace App\Service\Mail;

use App\Service\MessageFormatterInterface;
use App\Service\MessageUtils;
use Symfony\Component\DependencyInjection\Attribute\AutowireCallable;

class Mailer
{
    public function __construct(
        #[AutowireCallable(service: MessageUtils::class, method: 'format')]
        private MessageFormatterInterface $formatter
    ) {
    }

    public function sendMail(string $message, array $parameters): string
    {
        $formattedMessage = $this->formatter->format($message, $parameters);

        // ...
    }
}

除了使用 #[AutowireCallable] 属性外,您还可以通过配置为函数式接口生成适配器

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

    # ...

    app.message_formatter:
        class: App\Service\MessageFormatterInterface
        from_callable: [!service {class: 'App\Service\MessageUtils'}, 'format']

这样做,Symfony 将生成一个类(也称为适配器),该类实现 MessageFormatterInterface,它会将 MessageFormatterInterface::format() 的调用转发到您的底层服务方法 MessageUtils::format(),并带有其所有参数。

本作品,包括代码示例,根据 Creative Commons BY-SA 3.0 许可协议获得许可。
目录
    版本