文档目录
模型关注点(Concerns)

简介

本文件系统性说明 DouPHP 框架在模型层采用的"关注点(Concerns)"设计模式,聚焦于通过 PHP Trait 机制实现的功能复用。文档围绕以下四个核心关注点展开:

  • ContentMutators:内容字段的自动处理(标题、关键词、描述、排序、时间、价格等写入前规范化),现已优化换行符处理机制。
  • HasCategoryFilter:按分类筛选(含子孙分类),支持单分类、多分类、逗号字符串等多种入参形式。
  • HasPageTree:单页 page 树构建与扁平列表输出,内置进程内缓存与多语言预热。
  • HasDistinctValues:字段去重值获取与 class 列表生成,支持附加 WHERE 条件与当前值高亮。

同时说明这些关注点如何被业务模型组合使用,以及如何开发自定义关注点。

项目结构

DouPHP 将通用模型能力以 Trait 的形式集中在 core/model/concerns 目录下,业务模块的 Model 通过 use 引入所需 Trait,从而在不侵入业务逻辑的前提下获得统一的数据处理能力。

graph TB
subgraph "核心关注点"
CM["ContentMutators"]
HCF["HasCategoryFilter"]
HPT["HasPageTree"]
HDV["HasDistinctValues"]
HCT["HasCategoryTree"]
end
subgraph "业务模型示例"
M1["Article"]
M2["Brand"]
M3["Cases"]
M4["Course"]
M5["Download"]
M6["Gallery"]
M7["Item"]
M8["Equipment"]
M9["Faq"]
M10["Job"]
M11["Certificate"]
M12["Show"]
end
CM --> M1
HCF --> M1
HCF --> M3
HCF --> M4
HCF --> M5
HCF --> M6
HCF --> M7
HDV --> M2
HDV --> M8
HDV --> M9
HDV --> M10
HDV --> M11
HPT -.-> M1
HCT -.-> M1
CM --> M12

核心组件

  • ContentMutators:提供一组属性写入器(如 setTitleAttribute、setSlugAttribute、setDefinedAttribute、setKeywordsAttribute、setDescriptionAttribute、setSortAttribute、setAddTimeAttribute、setPriceAttribute、setPromotePriceAttribute),在数据落库前对内容进行清洗、格式化与类型转换,确保入库数据一致性与安全性。已优化换行符处理机制,提高性能和一致性。
  • HasCategoryFilter:提供 scopeFilterByCategory 查询作用域,支持传入 int、数字字符串、逗号分隔字符串或数组,自动展开为子孙分类 ID 集合并追加 where('category_id', 'IN', ...) 过滤。
  • HasPageTree:提供静态方法 pageTree 与 pageNolevel,用于构建单页嵌套树与无层级缩进列表;内部通过 pageRows 进行进程内缓存与多语言预热,避免重复 IO。
  • HasDistinctValues:提供 distinctValues 与 classList 静态方法,用于取某字段去重值集合(可附加 WHERE 片段),并返回带 cur 高亮标记的结构化选项;classList 进一步封装 class 字段并生成路由 URL。
  • HasCategoryTree:提供 tree 与 flat 静态方法,用于从 {module}_category 表构建分类树与扁平列表,包含多语言预热、图标 URL 解析与路由生成。

架构总览

下图展示了关注点在业务模型中的组合方式与调用链:业务模型通过 use 引入多个 Trait,组合出统一的查询与数据处理能力;其中 HasCategoryFilter 依赖 ORM Builder 与 CategoryIds 工具完成拓扑展开,HasPageTree 与 HasCategoryTree 依赖 DB Facade 与 language 服务完成缓存与多语言预热。

sequenceDiagram
participant C as "控制器/服务"
participant M as "业务模型(Article)"
participant T1 as "Trait : HasCategoryFilter"
participant B as "ORM Builder"
participant U as "Support : CategoryIds"
participant D as "DB Facade"
C->>M : 调用查询(含分类参数)
M->>T1 : scopeFilterByCategory(query, catIds)
T1->>B : 追加 where('category_id','IN',...)
T1->>U : subtree(table_category, cid)
U-->>T1 : 返回子孙ID集合
T1-->>M : 返回已过滤的Builder
M-->>C : 执行查询并返回结果

详细组件分析

ContentMutators:内容字段自动处理

  • 职责:在写入阶段对常见字段进行标准化处理,包括去除空白、换行符转逗号、数值校验、时间戳转换、价格字段清理等。已优化换行符处理机制,采用统一的标准化策略。
  • 关键方法:setTitleAttribute、setSlugAttribute、setDefinedAttribute、setKeywordsAttribute、setDescriptionAttribute、setSortAttribute、setAddTimeAttribute、setPriceAttribute、setPromotePriceAttribute。
  • 复杂度:O(1) 每字段处理;整体写入开销与字段数量线性相关。
  • 优化建议:若存在大量文本字段,可在应用层做批量预处理或使用数据库侧函数减少 PHP 层计算。
  • 错误处理:对空值与非数字输入进行安全回退,避免异常中断。
  • 更新:show模块的text字段处理现在使用统一的换行符标准化策略,提高了性能和一致性。
flowchart TD
Start(["写入触发"]) --> Title["处理标题<br/>trim"]
Title --> Slug["处理slug<br/>trim或空"]
Slug --> Defined["处理defined<br/>换行转逗号"]
Defined --> Keywords["处理keywords<br/>字符串化"]
Keywords --> Desc["处理description<br/>字符串化"]
Desc --> Sort["处理sort<br/>数值校验"]
Sort --> AddTime["处理add_time<br/>时间戳转换"]
AddTime --> Price["处理price<br/>trim"]
Price --> Promote["处理promote_price<br/>trim"]
Promote --> End(["完成写入"])

HasCategoryFilter:分类过滤(含子孙)

  • 职责:根据传入的分类 ID(支持 int、数字字符串、逗号字符串、数组)展开为所有子孙分类 ID 集合,并追加 where 条件。
  • 关键点:
    • 入参归一化:字符串拆分、非数组包装、过滤非法值。
    • 拓扑展开:委托 CategoryIds::subtree 获取子孙 ID。
    • 表名推导:基于宿主模型的 getTable() 拼接 _category。
  • 复杂度:取决于分类树深度与子节点数量;通常 O(N) 遍历。
  • 错误处理:空/0/空数组直接透传,不添加 where。
flowchart TD
S(["scopeFilterByCategory"]) --> Parse["解析入参<br/>int/字符串/数组"]
Parse --> Clean["过滤非法值<br/>intval & >0"]
Clean --> Empty{"是否为空?"}
Empty -- 是 --> ReturnQ["返回原query"]
Empty -- 否 --> Expand["展开子孙ID<br/>CategoryIds::subtree"]
Expand --> Unique["去重并整型化"]
Unique --> Where["追加where('category_id','IN',ids)"]
Where --> R(["返回新query"])

HasPageTree:单页树与扁平列表

  • 职责:提供 pageTree(嵌套树)与 pageNolevel(扁平列表)两个静态方法,用于渲染导航或下拉选择。
  • 关键点:
    • 进程内缓存:pageRows 静态变量缓存全量 page 行,避免重复查询。
    • 多语言预热:language()->warmup 预加载 name 字段的多语言值。
    • 递归构建:pageTree 递归组装 child 节点;pageNolevel 累积扁平项并记录层级 mark。
  • 复杂度:构建树 O(N^2) 朴素实现(因嵌套查找),可通过索引优化至 O(N)。
  • 错误处理:非数组行跳过;查询失败时返回空数组。
sequenceDiagram
participant V as "视图/控制器"
participant P as "HasPageTree"
participant D as "DB Facade"
participant L as "language 服务"
V->>P : pageTree(parentId, currentId)
P->>P : pageRows()
P->>D : 读取page全量数据
D-->>P : 返回数组
P->>L : warmup(name)
L-->>P : 完成预热
P->>P : 递归构建child/cur/url
P-->>V : 返回树结构

HasDistinctValues:字段去重与 class 列表

  • 职责:提供 distinctValues 与 classList 静态方法,用于获取字段去重值集合与 class 列表(含跳转 URL)。
  • 关键点:
    • 字段存在性检查:不存在则返回空数组,避免抛错。
    • 可选 WHERE 片段:支持原生 SQL 片段进行过滤。
    • 结构化输出:返回 value 与 cur 高亮标记;classList 额外生成 route URL。
  • 复杂度:O(N) 扫描与去重。
  • 错误处理:字段不存在或查询失败均安全返回空数组。
flowchart TD
A(["distinctValues(field, current, oneLevel, where)"]) --> Check["检查字段是否存在"]
Check --> |不存在| RetEmpty["返回[]"]
Check --> |存在| Query["SELECT field FROM table [WHERE]"]
Query --> Collect["收集值并去重"]
Collect --> OneLevel{"oneLevel ?"}
OneLevel -- 是| 扁平数组 --> RetFlat["返回扁平数组"]
OneLevel -- 否| 结构化 --> Build["构建[value, cur]结构"]
Build --> RetStruct["返回结构化数组"]

HasCategoryTree:分类树与扁平列表

  • 职责:提供 tree 与 flat 静态方法,用于从 {module}_category 表构建分类树与扁平列表,包含多语言预热、图标 URL 解析与路由生成。
  • 关键点:
    • 表名推导:从 getTable() 推断 module 名称,生成 category 路由。
    • 进程内缓存:按表名缓存分类行,避免重复 IO。
    • 多语言预热:name、description 字段预热。
  • 复杂度:树构建 O(N^2) 朴素实现;可优化为 O(N)。
  • 错误处理:非数组行跳过;空数据返回空数组。
classDiagram
class HasCategoryTree {
+static tree(currentId) array
+static flat(currentId, mark) array
-static categoryRows(table) array
-static categoryTreeWalk(table, parentId, currentId, level) array
-static categoryFlatWalk(table, parentId, level, currentId, acc, mark) void
}

Show模块text字段处理优化

  • 职责:show模块的前台和后台模型都实现了统一的text字段处理机制,确保不同操作系统下的换行符得到一致处理。
  • 关键改进:
    • 标准化换行符:使用 str_replace(array("\r\n", "\r"), "\n", $text) 统一换行符格式。
    • 性能优化:减少了多次字符串替换操作,提高了处理效率。
    • 一致性保证:确保PC端和小程序端的显示效果一致。
  • 应用场景:主要用于幻灯片的文字内容处理,支持多行文本的展示。
flowchart TD
Text["原始text字段"] --> Normalize["标准化换行符<br/>str_replace(array('\\r\\n', '\\r'), '\\n', text)"]
Normalize --> Split["按换行符分割<br/>explode('\\n', text)"]
Split --> Array["生成text_array"]
Array --> Output["输出到前端"]

依赖关系分析

  • 业务模型与关注点的组合:
    • Article(前台/后台):use ContentMutators、HasCategoryFilter。
    • Cases(前台/后台):use ContentMutators、HasCategoryFilter。
    • Course(前台/后台):use ContentMutators、HasCategoryFilter。
    • Download(前台/后台):use ContentMutators、HasCategoryFilter。
    • Gallery(前台/后台):use ContentMutators、HasCategoryFilter。
    • Item(前台/后台):use ContentMutators、HasCategoryFilter。
    • Brand(前台):use HasDistinctValues。
    • Equipment(前台):use HasDistinctValues。
    • Faq(前台/后台):use HasDistinctValues。
    • Job(后台):use HasDistinctValues。
    • Certificate(前台):use HasDistinctValues。
    • Show(前台/后台):使用优化的text字段处理机制。
graph LR
CM["ContentMutators"] --> ArtA["Article(后台)"]
CM --> ArtF["Article(前台)"]
CM --> CasA["Cases(后台)"]
CM --> CasF["Cases(前台)"]
CM --> CouA["Course(后台)"]
CM --> CouF["Course(前台)"]
CM --> DowA["Download(后台)"]
CM --> DowF["Download(前台)"]
CM --> GalA["Gallery(后台)"]
CM --> GalF["Gallery(前台)"]
CM --> IteA["Item(后台)"]
CM --> IteF["Item(前台)"]
HCF["HasCategoryFilter"] --> ArtF
HCF --> CasF
HCF --> CouF
HCF --> DowF
HCF --> GalF
HCF --> IteF
HDV["HasDistinctValues"] --> BraF["Brand(前台)"]
HDV --> EquF["Equipment(前台)"]
HDV --> FaqF["Faq(前台)"]
HDV --> FaqA["Faq(后台)"]
HDV --> JobA["Job(后台)"]
HDV --> CerF["Certificate(前台)"]
SH["Show模块"] --> TextOpt["text字段优化"]

性能考量

  • ContentMutators:写入阶段逐字段处理,开销与字段数线性相关;建议在批量写入时合并处理以减少多次赋值。已优化换行符处理机制,提高了处理效率。
  • HasCategoryFilter:分类拓扑展开可能产生较大 ID 集合;当分类树较深或分支较多时,注意 IN 列表长度限制与索引使用(category_id 应建立索引)。
  • HasPageTree:pageRows 进程内缓存有效降低重复查询;但 pageTree 的嵌套构建为 O(N^2),若页面数据量大,可考虑改为一次扫描建索引再线性构建。
  • HasDistinctValues:去重操作为 O(N);当字段基数大且频繁调用时,可结合缓存策略(如 Redis)减少数据库压力。
  • HasCategoryTree:分类树构建同样存在 O(N^2) 风险;建议对 parent_id 建立索引,并在高频场景下使用进程内缓存或多级缓存。
  • Show模块优化:统一的换行符处理减少了重复计算,提高了多平台兼容性。

故障排查指南

  • 分类过滤无效:
    • 检查传入分类 ID 是否合法(正整数);空/0/空数组会透传不落 where。
    • 确认宿主模型实现了 getTable() 且表名正确({table}_category)。
    • 验证 category_id 字段存在并有合适索引。
  • 去重值为空:
    • 检查字段是否存在;不存在时返回空数组。
    • 检查 WHERE 片段是否正确;错误可能导致无结果。
  • 单页树为空:
    • 检查 page 表是否有数据;确认 language 预热是否成功。
    • 检查 parent_id 层级关系是否正确。
  • 多语言显示异常:
    • 确认 language()->warmup 已调用且语言包配置正确。
    • 检查 translatableModule 与字段映射是否匹配。
  • Show模块text字段问题:
    • 检查换行符处理逻辑是否正确执行。
    • 确认不同操作系统下的换行符都能被正确识别和处理。
    • 验证前端显示是否符合预期。

结论

DouPHP 通过 Concerns(Trait)将模型层的通用能力解耦为独立模块,既提升了代码复用性,又保持了业务模型的简洁与专注。ContentMutators 保障数据一致性,现已优化换行符处理机制提高性能;HasCategoryFilter 简化复杂分类查询,HasPageTree 与 HasCategoryTree 提供高效树构建与多语言支持,HasDistinctValues 提供灵活的字段去重与选项生成。业务模型通过组合多个 Trait 即可快速获得丰富能力,同时便于扩展与维护。Show模块的text字段处理优化进一步提升了系统的性能和一致性。

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

在业务模型中引入关注点

  • 引入 ContentMutators:适用于需要统一内容处理的模型(如文章、课程、下载、画廊、商品等)。
  • 引入 HasCategoryFilter:适用于需要按分类(含子孙)筛选的模型(如文章、案例、课程、下载、画廊、商品等)。
  • 引入 HasDistinctValues:适用于需要字段去重与选项生成的模型(如品牌、设备、FAQ、职位、证书等)。
  • 引入 HasPageTree / HasCategoryTree:适用于需要构建单页树或分类树的模型(如文章分类、单页导航等)。
  • Show模块最佳实践:使用统一的换行符处理机制,确保跨平台兼容性。

使用示例(路径指引)

  • 分类过滤:在模型中使用 scopeFilterByCategory(query, catIds) 进行筛选,catIds 支持多种格式。
    • 参考:HasCategoryFilter.php:53-75
  • 字段去重:使用 distinctValues(field, currentValue, oneLevel, where) 获取选项列表,或 classList(class) 获取带 URL 的 class 列表。
    • 参考:HasDistinctValues.php:46-106
  • 单页树:使用 pageTree(parentId, currentId) 构建嵌套树,或使用 pageNolevel(parentId, level, currentId, acc, mark) 构建扁平列表。
    • 参考:HasPageTree.php:38-115
  • 分类树:使用 tree(currentId) 构建分类树,或使用 flat(currentId, mark) 构建扁平列表。
    • 参考:HasCategoryTree.php:44-65
  • Show模块text字段处理:使用统一的换行符标准化策略处理多行文本。
    • 参考:Show.php(前台):99-102
    • 参考:Show.php(后台):98-101

自定义关注点开发方法

  • 创建新的 Trait:在 core/model/concerns 下新增 .php 文件,定义公共方法与属性。
  • 抽象契约:如需强制宿主模型实现某些方法(如 getTable),使用 abstract public function 声明。
  • 组合使用:在业务模型中通过 use 引入多个 Trait,按需组合功能。
  • 测试与文档:为新关注点编写单元测试与使用说明,确保稳定性与可维护性。
  • 最佳实践:参考ContentMutators的换行符处理优化,确保跨平台兼容性。
添加日期:2026-10-05