文档目录
上下文绑定系统

简介

本文件面向 DouPHP 依赖注入容器的“上下文绑定”能力,围绕 when()->needs()->give() 链式 API 的设计理念、实现机制与作用域限制进行系统化说明。重点解释:

  • 上下文绑定如何仅对特定消费者类生效,而不影响全局绑定;
  • ContextualBindingBuilder 的构建器模式与状态管理;
  • 解析阶段如何通过“构建栈”定位当前消费者并应用上下文映射;
  • 与全局绑定(bind/singleton/factory/instance)的优先级与冲突解决策略;
  • 性能特征、适用场景与最佳实践建议。

项目结构

上下文绑定能力由容器核心与辅助函数共同构成:

  • 容器核心位于 core/foundation/container/Container.php,包含 Container 与 ContextualBindingBuilder;
  • 辅助函数位于 core/foundation/container/helpers.php,提供 app() 等便捷入口;
  • 启动期通过 core/bootstrap.php 将关键对象以单例形式注册到容器;
  • 路由层在 core/web/routing/DelegatingRouter.php 中通过容器获取 Request 实例,体现容器在框架中的早期可用性。
graph TB
A["启动引导<br/>core/bootstrap.php"] --> B["容器实例<br/>Container::getInstance()"]
B --> C["Request 单例注册"]
B --> D["DelegatingRouter 单例注册"]
E["业务代码"] --> F["app()/Container::make()"]
F --> G["解析依赖"]
G --> H{"是否上下文绑定?"}
H -- 是 --> I["按消费者类选择具体实现"]
H -- 否 --> J["走全局绑定/默认解析"]

核心组件

  • Container:轻量级 DI 容器,支持 bind/singleton/factory/instance、别名、has/bound/resolved、reset 等能力;内置上下文绑定 with when()->needs()->give()。
  • ContextualBindingBuilder:when() 返回的构建器,用于声明“某消费者类在某抽象参数上的具体实现”。
  • helpers.php:提供 app() 等便捷方法,统一从容器解析实例。
  • bootstrap.php:在框架启动早期将 Request、DelegatingRouter 等关键对象以单例形式注册,确保后续解析可用。

架构总览

上下文绑定的整体流程如下:

  • 配置阶段:通过 Container::when($consumer)->needs($abstract)->give($concrete) 声明“当消费者 $consumer 需要抽象 $abstract 时,使用 $concrete”。
  • 解析阶段:Container::make() 解析目标类时,进入 build() 并通过 resolveDependencies() 处理构造函数参数。
  • 作用域判定:resolveDependencies() 调用 resolveContextual(),仅查看当前构建栈顶的消费者(直接消费者),若存在上下文绑定则优先使用。
  • 回退策略:若无上下文绑定,则按全局绑定/工厂/单例/默认反射解析。
sequenceDiagram
participant U as "调用方"
participant C as "Container"
participant B as "build()"
participant R as "resolveDependencies()"
participant X as "resolveContextual()"
U->>C : make(抽象或类名, 参数)
C->>C : 别名/工厂/单例判断
C->>B : build(具体类, 参数)
B->>R : 解析构造函数参数
R->>X : 查找当前消费者的上下文绑定
alt 找到上下文绑定
X-->>R : 返回 concrete|callable
R->>C : 按需 make(concrete) 或调用工厂
else 未找到
R->>C : 递归 make(类型提示类)
end
R-->>B : 组装依赖数组
B-->>U : 返回实例

详细组件分析

Container:上下文绑定解析与优先级

  • 上下文绑定存储:contextualBindings[消费者][抽象] => 具体类或闭包。
  • 解析优先级(从高到低):
    1. 别名转发;
    2. 工厂闭包;
    3. 已缓存的单例;
    4. 全局绑定或默认类名;
    5. 反射构建依赖;
    6. 依赖解析过程中,针对当前消费者参数的上下文绑定优先于全局绑定。
  • 构建栈:buildStack 记录当前正在构建的消费者类名,用于 resolveContextual() 精确定位“直接消费者”,避免父级声明污染子依赖。
flowchart TD
Start(["开始解析"]) --> Alias["别名转发?"]
Alias -- 是 --> MakeAlias["make(目标抽象)"]
Alias -- 否 --> Factory["工厂?"]
Factory -- 是 --> CallFactory["调用工厂闭包"]
Factory -- 否 --> Singleton["单例已缓存?"]
Singleton -- 是 --> ReturnInstance["返回缓存实例"]
Singleton -- 否 --> Concrete["确定具体类"]
Concrete --> Build["反射构建依赖"]
Build --> Resolve["逐个参数解析"]
Resolve --> Contextual{"当前消费者有上下文绑定?"}
Contextual -- 是 --> UseCtx["使用上下文实现"]
Contextual -- 否 --> Recur["递归 make(类型提示)"]
UseCtx --> NextParam["下一个参数"]
Recur --> NextParam
NextParam --> Done(["完成"])

ContextualBindingBuilder:构建器模式与状态管理

  • 职责:封装 when()->needs()->give() 的链式声明,维护当前消费者与抽象,最终写入容器的上下文映射。
  • 状态字段:
    • container:持有容器引用,用于写入上下文绑定;
    • consumer:当前消费者类全限定名;
    • abstract:当前声明的抽象参数类型。
  • 约束:必须先调用 needs() 再调用 give(),否则抛出逻辑异常,保证声明完整性。
classDiagram
class Container {
+when(consumer)
+addContextualBinding(consumer, abstract, concrete)
+make(abstract, parameters)
-build(concrete, parameters)
-resolveDependencies(params, overrides)
-resolveContextual(abstract)
}
class ContextualBindingBuilder {
-container : Container
-consumer : string
-abstract : string|null
+__construct(container, consumer)
+needs(abstract) : this
+give(concrete) : void
}
Container --> ContextualBindingBuilder : "when() 返回"
ContextualBindingBuilder --> Container : "写入上下文绑定"

作用域限制:仅对特定消费者类生效

  • 设计要点:resolveContextual() 仅读取构建栈顶的消费者(直接消费者),不向上回溯父级依赖。这意味着:
    • 为控制器 A 设置的上下文绑定不会影响控制器 B;
    • 也不会影响 A 所依赖的子服务在其他消费者中的解析行为;
    • 避免“父级声明意外影响子依赖”的副作用。
  • 实际意义:同一接口在不同消费者中可以有不同的具体实现,互不干扰。

与全局绑定的优先级与冲突解决

  • 优先级顺序(解析阶段):
    1. 别名;
    2. 工厂闭包;
    3. 已缓存单例;
    4. 全局绑定或默认类;
    5. 依赖解析过程中的上下文绑定(针对当前消费者)。
  • 冲突解决:
    • 当某个消费者声明了上下文绑定,该绑定会覆盖全局绑定在该消费者下的解析结果;
    • 其他消费者不受影响,仍遵循全局绑定;
    • 若同时存在工厂/单例,需结合 make() 分支顺序理解:工厂与单例在 make() 顶层优先,但上下文绑定作用于依赖参数解析阶段,因此对“依赖项的具体实现”具有更强的针对性控制力。

依赖关系分析

  • 启动期依赖:
    • bootstrap.php 在 Init 之前将 DelegatingRouter 与 Request 以单例形式注册,确保路由与请求元信息可被容器解析;
    • helpers.php 提供 app() 作为容器访问的统一入口。
  • 运行时依赖:
    • DelegatingRouter 通过 Container::getInstance()->make(Request::class) 获取请求对象,展示容器在路由阶段的可用性;
    • 业务代码通过 app() 或 Container::getInstance() 进行依赖解析与上下文绑定。
graph LR
BS["bootstrap.php"] --> CT["Container 单例"]
CT --> REQ["Request 单例"]
CT --> DR["DelegatingRouter 单例"]
APP["helpers.php::app()"] --> CT
DR --> CT

性能考虑

  • 解析路径优化:
    • 工厂与单例在 make() 顶层快速命中,减少反射开销;
    • 上下文绑定仅在依赖解析阶段检查,且只查构建栈顶,避免全量扫描;
    • 别名转发一次后继续走常规解析路径。
  • 反射成本:
    • 无构造函数直接 new;
    • 有构造函数时使用 ReflectionParameter 解析类型提示,必要时递归 make;
    • PHP 版本兼容分支避免 ReflectionException 导致额外开销。
  • 内存占用:
    • contextualBindings 按消费者维度组织,粒度细、查询快;
    • singletons 缓存已实例化对象,避免重复创建。

故障排查指南

  • 常见错误与原因:
    • 未先调用 needs() 就调用 give():触发逻辑异常,需调整声明顺序;
    • 无法解析参数:当参数既无默认值也不允许为空且未注册绑定/上下文绑定时,抛出运行时异常;
    • 上下文绑定未生效:确认消费者类名与声明一致,且处于构建栈顶(即直接消费者)。
  • 调试建议:
    • 使用 reset() 清空容器状态,隔离测试环境;
    • 通过 has()/bound()/resolved() 检查绑定与单例状态;
    • 在解析失败时关注异常消息中的“chain”信息,定位依赖链。

结论

DouPHP 容器的上下文绑定通过 when()->needs()->give() 提供了“按消费者类切换实现”的能力,其核心在于:

  • 构建器模式清晰表达声明意图;
  • 构建栈精确锁定直接消费者,确保作用域最小化;
  • 与全局绑定协同工作,形成“全局通用 + 局部特化”的灵活装配模型;
  • 在启动期与运行期均具备良好性能与稳定性。

附录:使用示例与最佳实践

典型用法示意(概念性示例)

  • 为不同控制器注入不同的日志实现:
    • 当控制器 A 需要 Logger 接口时,注入 FileLogger;
    • 当控制器 B 需要 Logger 接口时,注入 ApiLogger;
  • 为不同服务注入不同的缓存实现:
    • 前台服务使用 RedisCache;
    • 后台服务使用 MemcacheCache;
  • 为 FormRequest 注入场景参数:
    • 通过 __scene 区分 store/update,使校验规则差异化。

注意:以上为概念性示例,实际使用时请在初始化阶段调用 when()->needs()->give() 完成声明。

最佳实践

  • 明确作用域:仅在确实需要“按消费者切换实现”的场景使用上下文绑定,避免滥用;
  • 保持声明集中:将上下文绑定集中在模块或服务的初始化文件中,便于维护;
  • 避免循环依赖:上下文绑定不应引入循环引用,必要时使用工厂闭包延迟创建;
  • 测试隔离:使用 reset() 清理容器状态,确保用例间互不影响;
  • 文档化约定:为每个上下文绑定编写注释,说明消费者、抽象与实现的关系。
添加日期:2026-10-05