不推荐控制器直接调用容器获取服务,应改为构造函数显式声明依赖并由di容器自动注入;yii2需通过自定义基类桥接,结合自动装配与分层设计,确保控制器职责单一、可测性强、类型安全。

控制器直接调用容器(如 $app->get('userService') 或 Yii::$container->get())属于服务定位(Service Locator)模式,虽能快速获取依赖,但会隐藏真实依赖、破坏可测试性、削弱类型安全,**不推荐在控制器中直接调用容器**。真正可持续的重构方向是:**把隐式容器调用转为显式依赖声明,再交由 DI 容器统一管理**。
明确依赖关系,拒绝服务定位
控制器不该自己“找”服务,而应清晰表达“我需要什么”。直接调用容器会让单元测试难以模拟依赖,也掩盖了类的真实职责边界。
- ❌ 避免写:
$userService = Yii::$container->get(UserService::class); - ✅ 改为构造函数参数声明:
public function __construct(private UserService $userService) { ... } - 这样 IDE 能自动补全,PHPStan/ Psalm 可静态校验,测试时也能直接传入 Mock 对象
统一基类 + 构造函数注入(Yii2 场景)
Yii2 控制器默认构造签名固定($id, $module, $config),需通过自定义基类桥接容器与业务依赖。
- 创建
controllers/BaseController.php,按顺序接收$id、$module、业务服务、$config - 务必先赋值服务属性,再调用
parent::__construct(...) - 所有业务控制器继承该基类,即可自然获得类型安全、自动注入的服务实例
启用自动装配(Autowiring)降低配置负担
现代框架(Symfony 8、Laravel、Spring Boot)普遍支持基于类型提示的自动装配,无需逐个写配置。
- Symfony 8:用
#[AsService]属性标记类,容器自动解析构造参数 - Spring Boot:默认开启构造器注入,
@Service类中final字段会被自动注入 - 关键点:保持构造函数参数精简、类型明确,避免循环依赖或歧义类型
按职责分层,避免控制器膨胀
即使依赖注入到位,控制器仍应只做三件事:接收请求、调用服务、返回响应。复杂逻辑必须下沉。
- 用户登录流程不应在
actionLogin()里查库、发邮件、写日志——这些应封装进LoginService - 控制器只协调,不实现;只编排,不运算
- 这样重构后,控制器测试只需验证输入输出,服务层测试覆盖核心逻辑











