Symfony4 如何为 UserPasswordEncoderCommand 构造函数自动装配参数?

How does Symfony4 autowire arguments to UserPasswordEncoderCommand constructor?

这是一个关于 Symfony 4 的问题autowiring 仅使用 'array' 作为构造函数参数类型提示。我给出了一个具体的案例,但这可能对其他人有帮助,因为这种情况发生在几个 Symfony 包中。

此 Symfony 命令要求输入密码并对其进行编码:
php bin/console security:encode-password

此命令的代码在 vendor/symfony/security-bundle/Command/UserPasswordEncoderCommand.php
在 Symfony 4.2.3 中,这是 UserPasswordEncoderCommand 的构造函数:

class UserPasswordEncoderCommand extends Command
{
    protected static $defaultName = 'security:encode-password';

    private $encoderFactory;
    private $userClasses;

    public function __construct(EncoderFactoryInterface $encoderFactory, array $userClasses = [])
    {
        $this->encoderFactory = $encoderFactory;
        $this->userClasses = $userClasses;

        parent::__construct();
    }

Symfony 使用依赖注入来调用构造函数,传入自动确定的参数。上面的构造函数第一个参数 $encoderFactory 是使用类型提示 EncoderFactoryInterface 自动装配的。 我的问题是:第二个参数 $userClasses 是如何自动装配的? Symfony 如何知道数组应该包含什么?调试打印语句显示数组包含单个值 "App\Entity\User".

这是我的 config/packages/security.yaml

security:
    encoders:
        App\Entity\User:
            algorithm: argon2i

    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        # used to reload user from session & other features (e.g. switch_user)
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: true
            guard:
                authenticators:
                    - App\Security\LoginFormAuthenticator

            # activate different ways to authenticate

            # http_basic: true
            # https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate

            # form_login: true
            # https://symfony.com/doc/current/security/form_login_setup.html

            logout:
                path:   app_logout
                # Where to redirect after logout
                target: app_login

我认为以下三个文件对回答这个问题很重要。
vendor/symfony/security-bundle/Resources/config/console.xml 包含:

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    <services>
        <defaults public="false" />

        <service id="security.command.user_password_encoder" class="Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand">
            <argument type="service" id="security.encoder_factory"/>
            <argument type="collection" /> <!-- encoders' user classes -->
            <tag name="console.command" command="security:encode-password" />
        </service>
    </services>
</container>

vendor/symfony/security-bundle/Resources/config/security.xml 的一部分包含:

    <service id="security.encoder_factory" alias="security.encoder_factory.generic" />
    <service id="Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface" alias="security.encoder_factory" />

    <service id="security.user_password_encoder.generic" class="Symfony\Component\Security\Core\Encoder\UserPasswordEncoder">
        <argument type="service" id="security.encoder_factory"></argument>
    </service>

    <service id="security.password_encoder" alias="security.user_password_encoder.generic" public="true" />
    <service id="Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface" alias="security.password_encoder" />

vendor/symfony/security-bundle/DependencyInjection/SecurityExtension.php 的一部分包含:

function load(array $configs, ContainerBuilder $container)
...
    if (class_exists(Application::class)) {
        $loader->load('console.xml');
        $container->getDefinition('security.command.user_password_encoder')->replaceArgument(1, array_keys($config['encoders']));
    }

我问这个问题是因为我想使用 UserPasswordEncoderCommand.php 作为在我的数据库中创建管理员用户的命令的起点。这将授予第一个管理员用户使用网络浏览器登录的权限 创建其他用户。

我尝试将 vendor/symfony/security-bundle/Command/UserPasswordEncoderCommand.php 复制到 src/Command/AddUserCommand.php 并更改这些行:

< namespace Symfony\Bundle\SecurityBundle\Command;
---
> namespace App\Command;

< class UserPasswordEncoderCommand extends Command
---
> class AddUserCommand extends Command

<     protected static $defaultName = 'security:encode-password';
---
>     protected static $defaultName = 'app:add-user';

当我运行这个命令时:php bin/console app:add-user 出现此错误:

There are no configured encoders for the "security" extension.

这是因为构造函数 __construct(EncoderFactoryInterface $encoderFactory, array $userClasses = []) 仅使用一个参数调用,因此第二个参数默认为空数组。

这些文件似乎也需要从 vendor/symfony/security-bundle 复制到 src/

下的某个地方
DependencyInjection/SecurityExtension.php
Resources/config/console.xml

并以某种方式进行修改。 还有哪些文件需要复制到src/? https://symfony.com 下的Symfony 文档很好,但是我找不到描述这种情况的地方。

请注意此命令有效:

php bin/console app:add-user mypassword "App\Entity\User"

但我不希望用户必须键入此内容。如果 security.yaml 发生变化,这可能会发生变化。

在这种特殊情况下,作为配置过程的一部分,数据由 SecurityBundle 组装。当内核启动时,所有的包都被收集起来,它们各自的扩展,通常在 DependencyInjection 子文件夹中,将被注册。构建容器时,它会通过所有这些扩展,因此他们可以修改服务或添加自己的服务。这就是 Symfony 从 bundle 内部收集配置的方式,当主应用程序本身不知道它们存储在哪里,但 bundle 知道。

获取用户 classes 是由 Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension 在处理 security.yml 的 encoders 部分时完成的。如果您查看 security.yaml 的编码器部分,您会注意到每个编码器都分配给了一个 class 名称。然后该方法将使用此数组为该值创建适当的编码器并将 class 名称保留为键。该数组在 createEncoders 中传递给编码器工厂。之后它将从配置中获取相同的数组,使用 array_keys() 获取每个用户的名称 class 并将参数替换为偏移量 1,即 $userClasses,用于命令。

这个概念称为语义配置,在您自己的应用程序中,这可能有点矫枉过正,因为您必须定义配置,然后手动编写逻辑。相反,您可能只在 services.yaml.

的参数部分定义一个带有 class 名称的数组

如果您想传入其他服务,这些服务可能来自您的捆绑包不知道的其他捆绑包,还有另一种方法称为 CompilerPasses。在 CompilerPass 中,您可以收集所有 classes,例如使用特定标记,然后将它们的引用传递给服务的方法或构造函数。例如,这在 Symfony 内部用于注册 EventListeners 或使命令在控制台应用程序中可用。