返回

解决 Symfony 7 Autowire 接口多实例注入错误

php

解决 Symfony 7 中 'Cannot autowire service... references interface...' 错误:注入同一接口的多个实例

咱们在用 Symfony 开发的时候,依赖注入(Dependency Injection)是个好东西,特别是 Autowiring,省了不少事儿。但有时候,自动的未必都灵光,比如你遇到下面这个报错:

Cannot autowire service "App\Core\Validator\ValidatorService": argument "$validators" of method "__construct()" references interface "App\Core\Validator\ValidatorInterface" but no such service exists. Did you create an instantiable class that implements this interface?

这错误挺常见的,尤其是在你想把实现了同一个接口的多个服务 一起注入到另一个服务里的时候。别慌,这问题能解决,而且 Symfony 提供了优雅的办法。

问题在哪?

看看报错信息,它说 ValidatorService 的构造函数需要一个类型是 App\Core\Validator\ValidatorInterface 的参数 $validators,但是找不到这样一个 具体 的服务。

怪了,我们明明创建了 LoginMessageValidatorWelcomeMessageValidator,它们都实现了 ValidatorInterface 啊!

问题就出在这儿:

  1. 自动装配的困惑 :当你只要求 Symfony 注入一个 ValidatorInterface 时,它看到有好几个类(LoginMessageValidator, WelcomeMessageValidator)都实现了这个接口,它就懵了——“你要哪个?” 它没法替你做决定。除非只有一个实现类,否则自动装配就会失败。
  2. 构造函数的误导 :你的 ValidatorService 构造函数写的是 __construct(ValidatorInterface $validators)。这看起来是想要一个 单一ValidatorInterface 实例。但是,你的 validate 方法里却用了 foreach($this->validators as $validator),这又表明你其实想要的是一个包含 多个 ValidatorInterface 实例的 集合(比如数组或者迭代器)。

这两点加起来,就导致了上面那个 Cannot autowire service 的错误。Symfony 既不知道该给你哪个单一实例,也不知道你其实想要的是一个包含所有实例的集合。

怎么解决?用 Tagged Iterators!

Symfony 早就考虑到了这种“一对多”的注入场景。最佳实践是使用 Tagged Services (服务标签)配合 Iterators (迭代器)或者 Service Locators 。对于咱们这个场景,Tagged Iterators 是最直接、最常用的方式。

思路是:

  1. 告诉 Symfony,所有实现了 ValidatorInterface 的服务都打上一个“标签”(或者利用自动配置)。
  2. 修改 ValidatorService 的构造函数,明确告诉它,你要注入的是一个 可迭代 的集合,这个集合包含了所有打了特定标签(或实现了特定接口)的服务。

步骤一:确保服务被发现和自动标记

在 Symfony 6+(包括 7)中,默认的 services.yaml 配置通常已经开启了 autoconfigure 和 autowire。这意味着,只要你的服务类(LoginMessageValidator, WelcomeMessageValidator)放在了 Symfony 能扫描到的目录(比如 src/ 下),并且它们实现了某个接口 (ValidatorInterface),Symfony 通常会自动给它们打上基于接口名称的标签。

你可以检查一下 config/services.yaml 文件,确保类似下面的配置是存在的(这通常是默认的):

# 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'

    # 你可以显式为实现了特定接口的服务打标签
    # 但通常 autoconfigure: true 会自动完成这一步
    # _instanceof:
    #     App\Core\Validator\ValidatorInterface:
    #         tags: ['app.validator'] # 可以自定义标签名,但不必要,可以直接用接口名

    # 控制器、命令等其他配置...

这里的 autoconfigure: true 非常关键。它会自动检测到 LoginMessageValidatorWelcomeMessageValidator 实现了 ValidatorInterface,并给它们打上类似于 app.core.validator.validator_interface 这样的标签(基于接口的全限定名)。

如果你想更明确一点,或者关闭了 autoconfigure,你可以手动添加标签:

# config/services.yaml
services:
    # ... 其他配置 ...

    App\Core\Validator\LoginMessageValidator:
        tags: ['app.validator'] # 或者使用接口名作为标签 'App\Core\Validator\ValidatorInterface'

    App\Core\Validator\WelcomeMessageValidator:
        tags: ['app.validator'] # 保持标签一致

    # ... ValidatorService 的配置(如果需要手动配)...

但通常,只要 autoconfigure: true 开着,你啥都不用写,Symfony 会自动处理好。

步骤二:修改 ValidatorService 接收迭代器

现在,重头戏来了。修改 ValidatorService 的构造函数,让它接收一个 iterable(可迭代对象),而不是单一的 ValidatorInterface

<?php declare(strict_types = 1);

namespace App\Core\Validator;

use App\Core\MailStruct;
// 注意这里:我们不再 use ValidatorInterface,因为类型提示变成了 iterable
// use App\Core\Validator\ValidatorInterface; 
use Psr\Container\ContainerInterface; // 或者直接用 iterable

class ValidatorService
{
    /**
     * @var iterable<ValidatorInterface>  // 使用 PHPDoc 明确说明迭代器里是什么类型
     */
    private iterable $validators;

    /**
     * 构造函数现在接收一个可迭代对象,里面包含了所有实现了 ValidatorInterface 的服务
     * 
     * Symfony 看到类型提示是 iterable,并且参数名叫 $validators (可以自定义),
     * 它会查找所有标记了 'App\Core\Validator\ValidatorInterface' 标签的服务 (得益于 autoconfigure),
     * 并将它们作为一个迭代器注入进来。
     *
     * 如果你手动设置了标签,比如 'app.validator',可以使用 #[TaggedIterator('app.validator')] 属性
     * 或者在 YAML 中配置 !tagged_iterator app.validator
     *
     * @param iterable<ValidatorInterface> $validators 注入所有实现了 ValidatorInterface 的服务
     */
    public function __construct(iterable $validators) 
    {
        // $validators 现在是一个迭代器,比如 RewindableGenerator,包含了 LoginMessageValidator 和 WelcomeMessageValidator 的实例
        $this->validators = $validators;    
    }
    
    public function validate(MailStruct $mail): void
    {
        // 现在可以直接遍历 $this->validators 了
        // 里面的每个 $validator 都是 ValidatorInterface 的一个实例
        foreach ($this->validators as $validator) {
            // 这里可以加一步判断,只调用支持当前邮件类型的 validator
            if ($validator->support($mail->getType())) { // 假设 MailStruct 有 getType() 方法
                 $validator->validate($mail);
            }
        }
    }

    // 如果你的 MailStruct 没有 getType 方法,你可能需要在调用 validate 前获取 type
    // 或者在 ValidatorService 内部根据 MailStruct 的某些属性决定类型
    public function validateWithType(MailStruct $mail, string $type): void
    {
         foreach ($this->validators as $validator) {
            if ($validator->support($type)) {
                 $validator->validate($mail);
                 // 如果一种类型只有一个 Validator 支持,可以加上 break;
                 // break; 
            }
        }
    }
}

解释:

  1. 构造函数参数类型 : 将 ValidatorInterface $validators 改成了 iterable $validators。这是告诉 Symfony 的依赖注入容器:“我想要的不是一个具体的服务,而是 所有 符合某种条件(这里是实现了 ValidatorInterface 接口)的服务的集合,并且这个集合是可以迭代的。”
  2. 自动标记与注入 : 由于 autoconfigure: true,Symfony 自动为所有实现 ValidatorInterface 的服务打上了基于接口名称的标签。当它看到 ValidatorService 的构造函数需要一个 iterable 类型的参数,并且没有明确指定标签时,它会默认查找所有实现了紧跟其后类型提示(这里通过 PHPDoc @param iterable<ValidatorInterface> $validators 提供,或者如果参数名约定如 $validatorInterfaces Symfony也能推断)的服务,并将它们打包成一个迭代器注入。
  3. @param iterable<ValidatorInterface> : 这个 PHPDoc 注释非常重要。它不仅是为了代码可读性,还能帮助静态分析工具和 IDE 理解 $validators 变量里具体是什么类型的对象。
  4. validate 方法 : 原来的 foreach 循环现在可以正常工作了,因为 $this->validators 确实是一个可迭代的对象(通常是一个 Traversable 的实例,比如 Symfony\Component\DependencyInjection\Argument\RewindableGenerator),里面包含了 LoginMessageValidatorWelcomeMessageValidator 的实例。
  5. 增加 support 判断 : 在 validate 方法的循环里,通常需要调用每个 validator 的 support 方法,判断当前这个 validator 是否应该处理当前的 MailStruct。这样可以确保只有合适的 validator 会执行 validate 逻辑。你需要根据你的 MailStruct 如何携带类型信息来调整这部分代码。

(可选)进阶技巧:使用命名标签和 #[TaggedIterator] 属性

如果你不喜欢依赖 autoconfigure 的隐式行为,或者你需要更精细地控制哪些服务被注入,或者想给这些服务定义一个特定的标签名,你可以这样做:

  1. services.yaml 中定义标签:

    # config/services.yaml
    services:
        # ...
        _instanceof:
            App\Core\Validator\ValidatorInterface:
                tags: ['app.mail_validator'] # 定义一个清晰的标签名
        # 或者单独给每个服务打标签
        # App\Core\Validator\LoginMessageValidator:
        #     tags: ['app.mail_validator']
        # App\Core\Validator\WelcomeMessageValidator:
        #     tags: ['app.mail_validator']
        # ...
    
  2. ValidatorService 构造函数中使用 #[TaggedIterator] 属性:

    <?php declare(strict_types = 1);
    
    namespace App\Core\Validator;
    
    use App\Core\MailStruct;
    use Symfony\Component\DependencyInjection\Attribute\TaggedIterator; // 引入属性类
    
    class ValidatorService
    {
        private iterable $validators;
    
        /**
         * 使用 #[TaggedIterator] 明确指定要注入哪个标签的服务集合
         *
         * @param iterable<ValidatorInterface> $validators
         */
        public function __construct(
            #[TaggedIterator('app.mail_validator')] // 使用在 YAML 中定义的标签
            iterable $validators 
        ) {
            $this->validators = $validators;    
        }
    
        // validate 方法保持不变...
        public function validate(MailStruct $mail): void
        {
            foreach ($this->validators as $validator) {
                // 假设 MailStruct 有 getType 方法
                if ($validator->support($mail->getType())) {
                     $validator->validate($mail);
                }
            }
        }
    }
    
    

    使用 #[TaggedIterator] 属性(PHP 8.0+)或者之前的 !tagged_iterator YAML 语法,可以让依赖关系更加明确。这种方式在你需要注入多个不同类型的服务集合时特别有用。

(可选)考虑优先级

如果你的 Validators 需要按特定顺序执行怎么办?你可以在打标签的时候添加 priority 属性:

# config/services.yaml
services:
    # ...
    App\Core\Validator\LoginMessageValidator:
        tags:
            - { name: 'app.mail_validator', priority: 10 } # 数字越大,优先级越高,越先执行

    App\Core\Validator\WelcomeMessageValidator:
        tags:
            - { name: 'app.mail_validator', priority: 5 }  # 优先级较低

    # 或者通过 _instanceof 定义默认优先级,然后单独覆盖
    # _instanceof:
    #    App\Core\Validator\ValidatorInterface:
    #        tags:
    #            - { name: 'app.mail_validator', priority: 0 } # 默认优先级

当你注入 #[TaggedIterator('app.mail_validator')] iterable $validators 时,Symfony 会按照 priority 从高到低的顺序排列这些服务。

总结一下

遇到 Cannot autowire service... references interface... but no such service exists 错误,尤其是在尝试注入一个接口的多个实现时,根本原因通常是 Symfony 不知道该选哪个单一实例,或者你其实想要的是一个集合而不是单一实例。

最佳解决方案是修改你的服务构造函数,让它接收一个 iterable 类型的参数,并利用 Symfony 的服务标签(通常通过 autoconfigure 自动实现或手动配置/#[TaggedIterator] 属性明确指定)来注入所有实现了该接口的服务集合。

这样做不仅解决了报错,也更符合你想把一系列策略(Validators)聚合起来使用的设计意图,代码清晰,扩展性也好——以后再添加新的 ValidatorInterface 实现类,只要放在 src/ 下,它就会自动被包含在这个集合里,无需修改 ValidatorService 的代码。