简介
本技术文档聚焦 DouPHP 后台数据模型层,围绕 ORM 映射机制、关系定义、查询构建器、类型转换、事件扩展、性能优化与最佳实践进行系统化说明。目标是帮助开发者快速理解并高效使用模型层,编写可维护、高性能的数据访问代码。
项目结构
DouPHP 的 ORM 位于 core/orm 目录,采用“轻量级 ActiveRecord”设计:
- Model:实体基类,负责字段映射、属性读写、事件、全局作用域、连接解析等。
- Builder:查询构造器,封装链式查询、预加载、水合、PrefetchRunner 执行。
- Relation 体系:抽象关系基类及 HasOne/HasMany/BelongsToMany 等具体关系实现。
- CastResolver:字段类型转换器,统一读写的类型转换策略。
- Collection:集合容器,提供常用集合操作与模板兼容。
graph TB
subgraph "ORM 核心"
M["Model"]
B["Builder"]
C["Collection"]
end
subgraph "关系"
R["Relation(抽象)"]
H1["HasOne"]
HM["HasMany"]
BT["BelongsTo"]
end
subgraph "类型转换"
CR["CastResolver"]
end
M --> B
B --> C
M --> R
R --> H1
R --> HM
R --> BT
M --> CR
B --> CR
核心组件
- 模型基类 Model:声明表名、主键、批量写入白名单、casts、prefetchers、with、translatable;提供实例化、查询入口、属性读写、事件、全局作用域、连接解析等能力。
- 查询构造器 Builder:在底层 Connection 之上叠加 with 预加载、模型水合、PrefetchRunner 执行;支持 where/order/limit/paginate 等链式方法,并提供 afterHydrate 回调栈。
- 关系体系 Relation:抽象出 addEagerConstraints/getEager/match/getResults 契约;HasOne/HasMany/BelongsTo 分别实现一对一、一对多、从属关系的惰性取值与批量预加载。
- 类型转换 CastResolver:内置 int/float/bool/string/json/array/datetime/date/timestamp/attachment/data_lang 等 cast,并提供 apply/applySet 双方向转换。
- 集合 Collection:对 foreach/array/json_encode/Smarty 透明,提供 map/filter/pluck/keyBy/modelKeys 等常用操作。
架构总览
ORM 的工作流如下:
- 读取路径:Model::query() → Builder → 应用全局作用域 → 执行底层查询 → 水合为 Model 集合 → 执行 eager load → PrefetchRunner → afterHydrate 回调 → 返回结果。
- 写入路径:Model::create()/fill()->save() → 计算脏字段 → 调用底层 insert/update → 同步 original → 触发事件(如 deleting/deleted)。
- 关系路径:通过 Relation 抽象,支持惰性取值与批量预加载,避免 N+1 查询。
sequenceDiagram
participant U as "调用方"
participant M as "Model"
participant B as "Builder"
participant Q as "Connection(底层)"
participant R as "Relation"
participant P as "PrefetchRunner"
U->>M : query()
M-->>B : new Query
B->>B : applyGlobalScopes()
B->>Q : select()/where()/order()/...
Q-->>B : 原始行集
B->>B : hydrateRows()
B->>R : eagerLoadRelations()
R-->>B : 关联结果
B->>P : run(rows, model)
P-->>B : 完成
B-->>U : Collection<Model>
详细组件分析
模型基类 Model:ORM 映射与扩展点
- 字段映射与类型转换:通过 $casts 声明字段类型,读取时经 CastResolver::apply 转换,写入时经 CastResolver::applySet 转换。
- 属性读写优先级:已加载关系 → accessor(getXxxAttribute) → cast → 原始值 → 惰性关系;设置时优先 setXxxAttribute 修改器 → 可逆 cast 写入转换 → 原样写入。
- 脏字段检测:基于 original 快照对比,考虑 cast 宽松比较,避免误报。
- 事件系统:deleting/deleted 静态注册监听器,fireModelEvent 依次派发,任一返回 false 中止。
- 全局作用域:addGlobalScope/withoutGlobalScope/withoutGlobalScopes,在 Builder 终结读路径一次性应用。
- 连接解析:setConnectionResolver 注入 DB 门面根实例,getConnection 获取底层连接。
classDiagram
class Model {
-string table
-string primary
-array fillable
-array casts
-array prefetchers
-array with
-array translatable
-array attributes
-array original
-array relations
-bool exists
-bool timestamps
+newQuery() Builder
+create(attributes) Model
+destroy(id) bool|int
+setAttribute(key,value) Model
+getAttribute(key) mixed
+getDirty() array
+isDirty(key) bool
+bootIfNotBooted() void
+addGlobalScope(id,scope) void
+deleting(callback) void
+deleted(callback) void
}
查询构建器 Builder:条件、关联与聚合
- 链式查询:where/whereIn/order/limit/join/field/group/having 等方法透传到底层 Connection,返回 $this 保持链式。
- 终结方法:get/first/find/paginate 返回水合后的 Model/Collection,并执行 eager load 与 PrefetchRunner。
- 标量聚合:count/sum/avg/max/min/value/exists/column 等只读聚合方法在调用前应用全局作用域,保证与 get()->count() 一致。
- 预加载:with('a', 'b') / with('a.b') / with(['a' => function($q){}]) 归一化为 eagerLoad,支持嵌套与约束闭包。
- 全局作用域:applyGlobalScopes 在终结读路径一次性应用,避免重复。
flowchart TD
Start(["进入 Builder::__call"]) --> CheckScope{"是否存在 scope{Method}?"}
CheckScope --> |是| CallScope["调用模型 scope 方法"]
CallScope --> ReturnScope{"返回是否为 Builder?"}
ReturnScope --> |是| ReturnThis["返回新链 Builder"]
ReturnScope --> |否| ReturnSelf["返回 $this"]
CheckScope --> |否| IsAggregate{"是否只读聚合方法?"}
IsAggregate --> |是| ApplyGS["应用全局作用域"]
IsAggregate --> |否| DirectCall["直接透传底层 Connection"]
ApplyGS --> DirectCall
DirectCall --> Result{"返回是否为 Connection?"}
Result --> |是| ReturnThis
Result --> |否| ReturnResult["返回结果"]
关系体系:一对一、一对多、从属关系
- Relation 抽象:定义 addEagerConstraints/getEager/match/getResults 契约,支持 nested 与 constraint 预加载选项。
- HasOne:关联端持有外键指向父模型主键;批量预加载使用 whereIn(localKey)。
- HasMany:一对多,批量预加载使用 whereIn(foreignKey)。
- BelongsTo:从属关系,父模型持有外键指向关联模型主键;批量预加载使用 whereIn(localKey)。
classDiagram
class Relation {
-Model parent
-Model related
-string foreignKey
-string localKey
-array eagerKeys
-array nested
-callable constraint
+setEagerOptions(nested,constraint) Relation
+addEagerConstraints(models) void
+getEager() Collection
+match(models,results,name) void
+getResults() mixed
}
class HasOne
class HasMany
class BelongsTo
Relation <|-- HasOne
Relation <|-- HasMany
Relation <|-- BelongsTo
类型转换 CastResolver:读/写双向转换
- 读取方向:int/float/bool/string/json/array/datetime/date/timestamp/attachment/data_lang 等,将数据库原始值转换为业务就绪形态。
- 写入方向:仅对有明确逆变换的 cast 反向转换,纯展示 cast 原样返回,避免二次转换。
- 时间兼容:datetime/date/timestamp 读取兼容 int 时间戳与 DATETIME 字符串双态,写入统一产出 'Y-m-d H:i:s'。
flowchart TD
In(["输入 value + type"]) --> Split["拆分 type:param"]
Split --> Custom{"是否自定义 cast?"}
Custom --> |是| CallCustom["调用自定义回调"]
Custom --> |否| SwitchType{"内置类型分支"}
SwitchType --> IntFloat["int/float/bool/string"]
SwitchType --> JsonArray["json/array"]
SwitchType --> DateTime["datetime/date/timestamp"]
SwitchType --> Attachment["attachment/attachment_thumb"]
SwitchType --> DefinedPairs["defined_pairs"]
SwitchType --> DataLang["data_lang"]
IntFloat --> Out(["输出转换后值"])
JsonArray --> Out
DateTime --> Out
Attachment --> Out
DefinedPairs --> Out
DataLang --> Out
集合 Collection:模板与数组兼容
- 提供 first/last/map/filter/pluck/keyBy/modelKeys 等操作。
- 实现 ArrayAccess/IteratorAggregate/Countable/JsonSerializable,对 Smarty/foreach/json_encode 透明。
依赖关系分析
- Model 依赖 Builder、Relation、CastResolver、DB 门面。
- Builder 依赖 Model、Connection、PrefetchRunner、Relation。
- Relation 依赖 Model、Collection。
- CastResolver 依赖 Util、Log、language/attachment 服务。
graph LR
Model["Model"] --> Builder["Builder"]
Model --> Relation["Relation"]
Model --> CastResolver["CastResolver"]
Builder --> Connection["Connection"]
Builder --> PrefetchRunner["PrefetchRunner"]
Relation --> Collection["Collection"]
CastResolver --> Util["Util"]
CastResolver --> Log["Log"]
性能考量
- 预加载优先:使用 with('relation') 或 with(['relation' => function($q){}]) 避免 N+1 查询。
- 只读聚合:count/sum/avg/max/min/value/exists/column 等聚合方法会应用全局作用域,确保一致性。
- 延迟加载:未显式 with 的关系会在访问时惰性加载,适合按需减少不必要查询。
- 字段选择:first(field) 或 field(...) 限制返回列,减少数据传输与内存占用。
- 分页:paginate 结合 with 预加载,注意分页中子关系约束的合理性。
- 类型转换:合理使用 casts 减少业务层转换开销,避免重复计算。
故障排查指南
- 主键参数校验:find/whereKey 要求标量主键,传入 null/''/数组/对象会抛出异常,防止误取首行或 PHP 类型错误。
- 全局作用域冲突:若 count() 与 get()->count() 不一致,检查是否在 __call 透传前正确应用全局作用域。
- 事件中止删除:deleting 监听器返回 false 会中止删除流程,需确认回调逻辑。
- 类型转换失败:json_decode 失败会记录警告并返回空数组,检查存储格式与 cast 配置。
- 时间字段兼容:datetime/date/timestamp 读取兼容 int 与 DATETIME,确保迁移期间数据一致性。
结论
DouPHP 的 ORM 以轻量级 ActiveRecord 为核心,通过 Model/Builder/Relation/CastResolver/Collection 的组合,提供了清晰的字段映射、关系定义、查询构建与类型转换能力。借助预加载、全局作用域、事件与修改器,开发者可以高效地实现复杂业务逻辑,同时保持良好的可维护性与性能。
附录:开发示例与最佳实践
-
创建新模型
- 继承 Model,声明 $table/$primary/$fillable/$casts/$with/$prefetchers。
- 使用 Model::create() 或 new Model()->fill()->save() 写入。
- 使用 Model::query()->where(...)->get() 读取。
-
定义关系
- 一对一:HasOne,关联端持有外键指向父模型主键。
- 一对多:HasMany,子端持有外键指向父模型主键。
- 从属关系:BelongsTo,父模型持有外键指向关联模型主键。
- 使用 with('relation') 或 with(['relation' => function($q){}]) 预加载。
-
高级查询
- 条件查询:where/whereIn/order/limit/join/field/group/having。
- 关联查询:with 嵌套关系与约束闭包。
- 聚合查询:count/sum/avg/max/min/value/exists/column。
-
扩展机制
- 模型事件:deleting/deleted 静态注册监听器。
- 访问器/修改器:getXxxAttribute/setXxxAttribute 钩子。
- 全局作用域:addGlobalScope/withoutGlobalScope/withoutGlobalScopes。
-
性能优化建议
- 优先预加载,避免 N+1。
- 限制返回字段,减少传输与内存。
- 合理使用分页与聚合。
- 使用 casts 统一类型转换,减少业务层重复处理。