简介
本文件聚焦 DouPHP 依赖注入容器的核心能力,面向初学者解释“什么是依赖注入、为什么需要容器”,同时为高级开发者深入剖析容器内部机制:服务注册(bind/singleton/factory)、实例解析(make)、构造函数自动注入(反射+类型提示+依赖链构建)、上下文绑定(when()->needs()->give())、状态查询(has/bound/resolved)与重置(reset),以及全局访问机制(单例)。文档以源码为依据,提供流程图与时序图帮助理解。
项目结构
DouPHP 的 DI 容器位于 core/foundation/container 下,包含核心类 Container 与一组便捷辅助函数 helpers.php;在 admin/api 启动阶段通过 Init.php 将各类服务注册到容器中,形成应用级装配。
graph TB
A["应用入口<br/>admin/api 启动"] --> B["容器初始化<br/>Container::getInstance()"]
B --> C["服务注册<br/>bind / singleton / factory / instance"]
C --> D["业务代码调用<br/>app()/make()"]
D --> E["解析与构建<br/>反射 + 依赖注入"]
E --> F["返回实例<br/>单例缓存/工厂/上下文绑定"]
核心组件
- 容器类 Container:实现服务注册、解析、生命周期管理、上下文绑定、状态查询与重置。
- 上下文绑定构建器 ContextualBindingBuilder:支持 when()->needs()->give() 的链式声明。
- 辅助函数 helpers.php:提供 app() 等便捷入口,统一从全局容器解析服务。
架构总览
容器采用“注册-解析”模式:
- 注册阶段:在应用启动时(admin/api Init)将服务以 bind/singleton/factory/instance 方式注册。
- 解析阶段:业务代码通过 app()/make() 请求服务,容器根据注册信息决定创建策略,并通过反射完成构造函数参数自动注入。
- 生命周期:singleton 与 instance 会缓存实例;factory 每次调用都会执行闭包;普通 bind 每次解析新建实例。
sequenceDiagram
participant App as "业务代码"
participant Helper as "app()"
participant C as "Container"
participant R as "反射/构建"
participant S as "服务实现"
App->>Helper : app(ClassName : : class, params)
Helper->>C : make(abstract, parameters)
alt 已注册工厂
C-->>App : 调用工厂闭包并返回
else 已缓存单例
C-->>App : 直接返回缓存实例
else 普通绑定或类名
C->>R : build(concrete, parameters)
R->>R : 解析构造函数参数反射+类型提示
R->>S : new Concrete(...)
R-->>C : 返回实例
opt 单例
C->>C : 缓存实例
end
C-->>App : 返回实例
end
详细组件分析
服务注册机制(bind、singleton、factory、instance)
- bind:将抽象(接口/别名/类名)映射到具体类或字符串类名,每次解析新建实例。
- singleton:标记为单例,首次解析后缓存实例,后续直接返回。
- factory:用闭包控制创建逻辑,覆盖先前 instance 缓存,每次 make 都执行工厂。
- instance:直接注入已有实例,覆盖先前 factory,之后 make 始终返回该实例。
flowchart TD
Start(["注册入口"]) --> Bind{"是否 bind?"}
Bind --> |是| SaveBind["保存 bindings[abstract] = concrete"]
Bind --> |否| Singleton{"是否 singleton?"}
Singleton --> |是| MarkSingle["标记 _singleton=true 并写入 singletons[abstract]=null"]
Singleton --> |否| Factory{"是否 factory?"}
Factory --> |是| SaveFactory["保存 factories[abstract] = callable"]
Factory --> |否| Instance{"是否 instance?"}
Instance --> |是| SaveInstance["保存 singletons[abstract] = $instance"]
Instance --> |否| End(["结束"])
实例解析算法(make)
make 的核心流程:
- 别名转发:若 abstract 是别名,则递归解析目标。
- 工厂优先:若存在工厂闭包,直接调用并返回。
- 单例命中:若为单例且已缓存,直接返回。
- 解析具体类:从 bindings 取 concrete,否则直接使用 abstract。
- 构建对象:调用 build 进行反射构造与依赖注入。
- 单例缓存:若为单例,缓存实例后返回。
flowchart TD
MStart(["make(abstract, parameters)"]) --> Alias{"有别名?"}
Alias --> |是| Recur["make(target, parameters)"]
Alias --> |否| FactoryCheck{"有工厂?"}
FactoryCheck --> |是| CallFactory["call_user_func(factory, container)"]
FactoryCheck --> |否| SingleCheck{"单例且已缓存?"}
SingleCheck --> |是| ReturnCached["返回 singletons[abstract]"]
SingleCheck --> |否| ResolveConcrete["concrete = bindings[abstract] ?? abstract"]
ResolveConcrete --> Build["build(concrete, parameters)"]
Build --> SingletonCache{"是否单例?"}
SingletonCache --> |是| Cache["singletons[abstract] = instance"]
SingletonCache --> |否| ReturnInst["返回 instance"]
构造函数自动注入(反射、类型提示、依赖链)
- 反射获取构造函数参数列表。
- 对每个参数:
- 优先使用传入的 overrides(含 __scene 等特殊键)。
- 读取类型提示(兼容 PHP 5.6/8.x),若为 FormRequest 子类则按场景构造。
- 检查上下文绑定(当前消费者 needs 指定实现)。
- 若无类型提示但有默认值或可空,则使用默认值或 null。
- 否则抛出运行时异常,错误信息包含依赖链(buildStack)。
- 使用 ReflectionClass::newInstanceArgs 组装依赖并创建实例。
flowchart TD
BStart(["build(concrete, parameters)"]) --> Refl["ReflectionClass(concrete)"]
Refl --> CheckInstantiable{"可实例化?"}
CheckInstantiable --> |否| ThrowNotInst["抛出不可实例化异常"]
CheckInstantiable --> |是| GetCtor["getConstructor()"]
GetCtor --> HasCtor{"有无构造函数?"}
HasCtor --> |无| NewDirect["new concrete()"]
HasCtor --> |有| PushStack["push buildStack"]
PushStack --> Resolve["resolveDependencies(params, overrides)"]
Resolve --> PopStack["pop buildStack"]
PopStack --> NewArgs["newInstanceArgs(dependencies)"]
NewArgs --> ReturnObj["返回对象"]
上下文绑定(when()->needs()->give())
- when(consumer) 返回 ContextualBindingBuilder。
- needs(abstract) 指定消费者需要的抽象。
- give(concrete|callable) 在当前消费者上下文中覆盖该抽象的实现。
- 解析时仅在 resolveContextual 中查看栈顶消费者的上下文映射,避免影响其他消费者。
sequenceDiagram
participant Dev as "开发者"
participant C as "Container"
participant B as "ContextualBindingBuilder"
Dev->>C : when(Consumer : : class)
C-->>Dev : B
Dev->>B : needs(Abstract : : class)
Dev->>B : give(Concrete : : class|callable)
B->>C : addContextualBinding(Consumer, Abstract, Concrete)
状态管理与重置(has/bound/resolved/reset)
- has/bound:判断抽象是否已注册(包括别名、绑定、工厂、单例)。
- resolved:判断单例是否已实例化。
- forgetInstance:移除指定单例实例(保留绑定,下次解析重建)。
- reset:清空所有绑定、单例、工厂、上下文、别名与构建栈,用于测试隔离或重启。
全局访问机制(单例)
- Container::getInstance:获取全局容器实例(延迟初始化)。
- Container::setInstance:替换全局容器(测试或托管启动)。
- helpers.php 中的 app():作为统一入口,返回容器本身或解析服务。
依赖关系分析
- 容器依赖反射 API(ReflectionClass/ReflectionMethod/ReflectionParameter)进行依赖解析。
- 与 FormRequest 集成:当参数类型为 FormRequest 子类时,通过场景参数构造。
- 与业务模块解耦:通过 ProviderRegistry 在各端 Init 中注册服务,使容器成为装配中心。
graph LR
C["Container"] --> R["反射API"]
C --> FR["FormRequest"]
C --> PR["ProviderRegistry(外部)"]
PR --> SvcA["语言服务(LanguageContract)"]
PR --> SvcB["插件服务(PluginServiceContract)"]
PR --> SvcC["消息响应(MessageResponderInterface)"]
性能考量
- 单例与实例缓存:singleton/instance 避免重复创建,降低开销。
- 工厂闭包:适合昂贵对象的按需创建,但注意频繁调用带来的成本。
- 反射开销:反射解析发生在首次构建时,后续可通过单例缓存减少重复反射。
- 上下文绑定:仅作用于当前消费者,避免全局污染,提高解析效率。
- 建议:
- 将重资源服务注册为 singleton 或 instance。
- 对可变状态的服务使用 factory 或 instance,避免共享状态导致并发问题。
- 合理拆分大依赖树,减少深层依赖链导致的构建成本。
故障排查指南
常见错误与定位方法:
- “Cannot resolve parameter [$name] in [Class]”:缺少依赖或未提供默认值/可空。检查依赖链(buildStack)与上下文绑定。
- “is not instantiable”:目标类不可实例化(如抽象类/接口未绑定具体实现)。
- 单例未生效:确认是否被 factory 覆盖或忘记注册为 singleton。
- 上下文绑定无效:确认 when()->needs()->give() 是否在正确消费者上下文中声明。
调试技巧:
- 使用 has/bound/resolved 检查注册与实例化状态。
- 使用 reset 清理容器状态,便于测试隔离。
- 在 make 前打印 buildStack 或临时记录日志,观察依赖解析顺序。
结论
DouPHP 的 DI 容器提供了轻量而强大的依赖注入能力:支持多种注册方式、自动构造函数注入、上下文绑定、状态查询与重置,并通过全局单例与辅助函数简化使用。结合各端 Init 的装配,容器成为系统服务的统一管理中心,既满足初学者易用性,又为高级用户提供深入控制点。
附录:使用示例与最佳实践
- 基本绑定与解析
- 将接口绑定到实现:在 Init 中使用 bind 或 provider 注册。
- 解析服务:通过 app(ClassName::class) 或 Container::getInstance()->make(...)。
- 单例与工厂
- 单例:singleton 或 instance 用于共享状态或昂贵对象。
- 工厂:factory 用于复杂创建逻辑或条件创建。
- 上下文绑定
- when(Consumer::class)->needs(Abstract::class)->give(Concrete::class) 针对特定消费者切换实现。
- 状态查询与重置
- has/bound 检查注册;resolved 检查单例实例化;reset 用于测试隔离。
- 全局访问
- 使用 app() 作为统一入口;必要时通过 Container::getInstance() 直接操作容器。