Facade是静态代理,通过__callStatic和getFacadeClass()懒加载容器实例;DI依赖容器反射注入,支持接口绑定、测试与生命周期管理。

ThinkPHP 中的依赖注入(DI)与 Facade(门面)看似都能“调用服务”,但底层机制、适用场景和设计意图完全不同。选错方式,轻则代码难测难换,重则埋下耦合隐患。
Facade 是静态代理,不是依赖注入
Facade 表面是静态调用,比如 Cache::get('key') 或 Db::table('user'),但它内部不经过类型解析,也不走构造函数注入流程。它靠 __callStatic 魔术方法触发,通过 getFacadeClass() 返回容器标识(如 'cache'),再由容器懒加载对应实例并转发调用。
这意味着:
- Facade 可在无容器上下文(如命令行早期阶段)运行,只要基类存在就能调用
- 它不参与生命周期管理,无法控制单例/多例或作用域
- 调用点不显式声明依赖,真实依赖被隐藏,重构和测试时容易遗漏
- getFacadeClass() 返回值必须是容器中已注册的标识符,写成完整类名却未绑定,会直接报 “Identifier not registered”
依赖注入靠容器驱动,支持真正解耦
依赖注入要求对象必须由容器创建,例如控制器方法参数带类型提示:public function index(UserService $service)。路由层通过反射读取类型,再调用 app()->make(UserService::class) 实例化并注入。
立即学习“PHP免费学习笔记(深入)”;
这种机制带来关键能力:
- 支持接口绑定:在 app/provider.php 中配置 'app\cache\CacheInterface' => 'app\cache\RedisCache',所有依赖该接口的地方自动切换实现
- 便于单元测试:可直接传入 Mock 对象,无需打桩静态方法
- 明确表达依赖关系:构造函数或方法参数即契约,谁用谁声明
- 配合作用域(request/scoped)可管理请求级实例生命周期
什么时候用 Facade,什么时候必须用 DI
Facade 适合工具型、无状态、辅助性操作:
- 日志记录:Log::info()
- 缓存读写:Cache::get()、Cache::set()
- 路由定义:Route::get()
- 中间件、事件监听器、命令行指令等轻量入口处
依赖注入适用于业务核心组件,尤其是需要可替换、可测试、有状态或需协调多个协作对象的场景:
- 用户注册服务:UserRegisterService(可能对接不同短信网关)
- 订单处理策略:OrderProcessor(需注入库存、支付、通知等具体实现)
- 仓储类:UserRepository(应依赖 UserInterface,而非具体数据库驱动)
混用风险与常见误区
在同一个类中同时使用 Facade 和 DI 容易出问题:
- ThinkPHP 默认不通过容器实例化控制器,若在 __construct() 中既写 Cache::get() 又期待 UserService $service 自动注入,后者很可能为 null
- Facade 绑定的是具体类(如 think\Cache),无法通过配置切换实现;而 DI + 接口绑定可零改动切换 Redis 缓存或文件缓存
- 写 Facade 类时误将 getFacadeClass() 返回 \app\common\MyService::class 字符串,却没在容器中注册该类,就会失败——正确做法是返回标识符(如 'my_service')并在容器中绑定



















