简介
本文件面向 DouPHP 模板引擎的“自定义函数(修饰器)”能力,系统性说明如何注册、定义、调用和调试自定义模板函数,涵盖函数文件结构、命名规范、参数传递机制、注册流程(含别名与版本兼容)、调用语法、参数绑定规则、返回值处理、安全验证与输入过滤、性能优化技巧等。文档基于仓库中的模板子系统源码进行梳理与归纳,确保与实际实现一致。
项目结构
DouPHP 的模板系统位于 core/web/template 下,围绕编译式渲染设计,关键路径如下:
- 引擎入口与生命周期:DouView
- 运行期上下文与函数分发:RenderContext
- 修饰器注册表:FilterRegistry
- 内置修饰器集合:StandardFilters
- 编译器管线:DouViewCompiler → ExpressionCompiler → TagCompiler
- 预处理器:FrontPrefilter(用于编译前对模板源做变换)
graph TB
A["DouView<br/>引擎主类"] --> B["RenderContext<br/>运行期上下文"]
A --> C["DouViewCompiler<br/>编译管线"]
C --> D["ExpressionCompiler<br/>表达式解析"]
B --> E["FilterRegistry<br/>修饰器注册表"]
E --> F["StandardFilters<br/>内置修饰器"]
A --> G["FrontPrefilter<br/>编译前预处理"]
核心组件
- FilterRegistry:维护“修饰器名 → callable”的映射,提供 register/has/get 方法。
- RenderContext:运行期唯一载体,提供 filter() 按名分发到已注册的修饰器;同时管理变量作用域、循环属性、子模板包含等。
- StandardFilters:集中注册内置修饰器(如 truncate、escape、date_format、default、replace 等),通过静态方法统一注入到 FilterRegistry。
- DouView:引擎主类,构造时自动注册内置修饰器;提供 assign/fetch/display/renderResource/compileSource 等方法;支持 prefilter 注册与执行;控制编译缓存与版本失效。
- DouViewCompiler:负责将模板源经 Lexer→Parser→CodeGenerator 编译为 PHP 代码;内部使用 ExpressionCompiler 解析表达式与修饰器链。
- EchoTagCompiler:输出标签编译器,负责把 {$var|modifier} 等表达式编译为可执行的 PHP 输出语句,并处理全局 HTML 转义开关与 nofilter 标记。
- FrontPrefilter:编译前对模板源做变换(例如主题资源路径替换),由 DouView 在编译阶段调用。
架构总览
下图展示了从模板源到最终输出的完整链路,以及自定义修饰器的参与点。
sequenceDiagram
participant T as "模板源"
participant V as "DouView"
participant P as "FrontPrefilter"
participant C as "DouViewCompiler"
participant E as "ExpressionCompiler"
participant R as "RenderContext"
participant F as "FilterRegistry"
participant S as "StandardFilters"
T->>V : fetch()/display()
V->>V : renderResource()
V->>P : runPrefilter(source)
P-->>V : 预处理后的source
V->>C : compile(resourceName, source)
C->>E : parseVarProps / compileIfCondition
E-->>C : 表达式AST/字符串
C-->>V : 生成PHP产物
V->>V : include(编译产物)
Note over V : 运行期 $ctx = RenderContext
V->>R : 输出时遇到 {$x|mod}
R->>F : get("mod")
F-->>R : callable
R->>S : 调用标准或自定义修饰器
S-->>R : 返回值
R-->>V : 输出结果
详细组件分析
修饰器注册表 FilterRegistry
- 职责:维护名称到可调用的映射,提供注册、查询、获取能力。
- 关键点:register(name, callable) 以字符串键存储;get(name) 返回 callable 或 null;has(name) 判断是否存在。
- 扩展性:任何 callable 均可注册,包括闭包、静态方法、实例方法数组等。
运行期上下文 RenderContext
- 职责:承载模板变量作用域、循环状态、子模板包含;提供 filter() 分发修饰器。
- 修饰器调用:filter($name, $value, ...$args) 将首参作为修饰器名,后续参数依次传入;若未找到则原值返回。
- 循环属性:loopProp() 支持 iteration/index/total/first/last/show。
- 子模板包含:includeTemplate() 支持变量隔离与捕获输出。
内置修饰器 StandardFilters
- 职责:集中注册常用修饰器(文本、日期、计数、格式化等)。
- 注册方式:registerInto(FilterRegistry) 将映射表逐项注册到注册表。
- 典型修饰器:truncate、escape、nl2br、strip_tags、date_format、default、indent、stringformat、strip、capitalize、cat、count*、lower、upper、replace、spacify、wordwrap。
- 兼容性:date_format 针对 PHP 8.1+ 做了 strftime 废弃兼容处理。
引擎 DouView
- 职责:模板渲染入口、编译管线协调、预处理器调度、编译缓存与版本控制。
- 初始化:构造时创建 FilterRegistry 并注册内置修饰器。
- 变量作用域:assign() 写入 vars;getAssigned() 供 prefilter 读取。
- 预处理器:registerPrefilter() 设置;runPrefilter() 在编译前执行。
- 渲染流程:fetch()/display() → renderResource() → 编译/缓存命中 → include 编译产物。
- 编译:compileSource() 先执行 prefilter,再交给 DouViewCompiler。
- 路径安全:resolveTemplatePath() 校验白名单与路径穿越。
编译器 DouViewCompiler 与表达式层
- 职责:将模板源转换为 PHP 代码;组织 Lexer→Parser→CodeGenerator。
- 表达式解析:ExpressionCompiler 提供 parseVarProps() 解析变量与修饰器链;compileIfCondition() 编译条件;compileTernary() 编译三元表达式;并提供 hasNofilterFlag()、hasHtmlEscapeModifier() 等辅助。
- 标签编译:TagCompilerRegistry 注册各类标签编译器(echo/url/include/assign/if/foreach/list/category/strip/literal/comment/php/delim/break/continue)。
输出与修饰器调用 EchoTagCompiler
- 职责:编译 {$var|modifier} 与三元表达式为 PHP 输出语句。
- 修饰器链:parseVarProps() 会解析命令与修饰器串;nofilter 标志可跳过默认修饰器。
- 全局转义:当开启 escapeHtml 且未显式使用 html 转义修饰器时,自动包裹 htmlspecialchars。
- 三元表达式:compileTernary() 返回 expr 与 nofilter 标志,交由调用方决定输出策略。
预处理器 FrontPrefilter
- 职责:编译前对模板源做变换,如主题静态资源路径替换、注释标签还原等。
- 上下文访问:通过 PrefilterContext 接口读取已 assign 的变量(如 site、theme_path)。
依赖关系分析
- DouView 依赖 FilterRegistry 与 StandardFilters,并在构造时完成内置修饰器注册。
- RenderContext 依赖 FilterRegistry 以在运行期分发修饰器。
- DouViewCompiler 依赖 ExpressionCompiler 与 TagCompilerRegistry,后者聚合各标签编译器。
- EchoTagCompiler 依赖 ExpressionCompiler 以解析表达式与修饰器链。
- FrontPrefilter 依赖 PrefilterContext 以读取编译前上下文。
classDiagram
class DouView {
+assign()
+fetch()
+display()
+renderResource()
+compileSource()
+registerPrefilter()
+runPrefilter()
}
class RenderContext {
+set()
+filter()
+loopProp()
+includeTemplate()
}
class FilterRegistry {
+register()
+has()
+get()
}
class StandardFilters {
+registerInto()
}
class DouViewCompiler {
+compile()
}
class ExpressionCompiler {
+parseVarProps()
+compileIfCondition()
+compileTernary()
+hasNofilterFlag()
+hasHtmlEscapeModifier()
}
class EchoTagCompiler {
+compile()
}
class FrontPrefilter {
+apply()
}
DouView --> FilterRegistry : "持有"
DouView --> StandardFilters : "注册内置"
RenderContext --> FilterRegistry : "运行时分发"
DouViewCompiler --> ExpressionCompiler : "使用"
EchoTagCompiler --> ExpressionCompiler : "解析表达式"
DouView --> FrontPrefilter : "编译前调用"
性能考虑
- 编译缓存与版本失效:DouView 使用 CompileCache,结合 COMPILE_REVISION 与 VERSION 控制缓存失效,避免重复编译。
- 预处理器开销:FrontPrefilter 在每次编译前执行,应尽量减少复杂正则或 I/O 操作。
- 修饰器链长度:表达式层支持修饰器链,过长链会增加解析与运行期调用成本,建议拆分逻辑或使用服务层。
- 全局转义:开启 escapeHtml 会在输出时额外包裹转义函数,按需启用以避免不必要的开销。
- 循环与包含:RenderContext 限制 include 深度,防止递归包含导致栈溢出;合理使用 includeTemplate 减少重复渲染。
故障排查指南
- 修饰器未生效:检查是否已通过 StandardFilters::registerInto 或自定义注册到 FilterRegistry;确认 RenderContext::filter 能获取到 callable。
- 参数不匹配:修饰器签名应为 function($value, $arg1, ...);多余或缺失参数可能导致异常或错误结果。
- 无过滤器标记无效:{$var|nofilter} 仅影响默认修饰器链;若需完全绕过修饰器,请调整表达式写法。
- 全局转义冲突:当开启 escapeHtml 且修饰器链中已包含 html 转义时,可能出现双重转义;可通过 nofilter 或显式修饰器控制。
- 预处理器问题:FrontPrefilter 修改的是模板源,注意不要破坏模板语法;必要时记录日志或临时关闭预处理器定位问题。
- 路径安全问题:模板资源名必须通过 resolveTemplatePath 校验,避免路径穿越与越界访问。
结论
DouPHP 模板系统的自定义函数(修饰器)通过 FilterRegistry 与 RenderContext 形成清晰的“编译期注册 + 运行时分发”机制。开发者可在 StandardFilters 基础上扩展自定义修饰器,并通过 DouView 的生命周期进行注册与集成。配合预处理器、表达式解析与标签编译器,系统提供了灵活而安全的模板扩展能力。遵循本文的注册规范、调用语法与安全实践,可有效提升模板的可维护性与性能。
附录:开发示例与最佳实践
自定义函数文件结构与命名规范
- 文件位置:建议放在 core/web/template/filter 或业务模块对应目录下,便于管理与加载。
- 命名规范:修饰器名建议使用小写加下划线(如 my_custom_filter),与内置修饰器风格保持一致。
- 函数签名:function($value, $arg1, $arg2, ...),第一个参数始终为待处理值,后续为冒号分隔的参数。
- 返回值:返回标量或字符串;如需返回数组或对象,请在模板中谨慎使用。
函数注册流程(含别名与版本兼容)
- 基本注册:在应用启动或模块初始化时,调用 FilterRegistry::register('my_filter', $callable)。
- 批量注册:参考 StandardFilters::registerInto,建立 name → callable 映射后循环注册。
- 别名设置:同一 callable 可多次 register 不同名称,实现别名效果。
- 版本兼容:利用 DouView::COMPILE_REVISION 与 VERSION 控制编译缓存失效;升级修饰器逻辑时,可 bump 版本号强制重编。
调用语法与参数绑定规则
- 调用语法:在模板中使用 {$var|my_filter:arg1:arg2},其中 var 为变量,my_filter 为修饰器名,后续为冒号分隔的参数。
- 参数绑定:RenderContext::filter 将首参作为修饰器名,后续参数依次传入 callable;未找到修饰器时返回原值。
- 修饰器链:支持多个修饰器串联,如 {$var|escape:html|truncate:50};nofilter 可跳过默认修饰器链。
- 返回值处理:修饰器返回值直接参与后续修饰器链或最终输出;三元表达式与输出策略由 EchoTagCompiler 控制。
完整开发示例(概念性步骤)
- 简单函数:实现一个将字符串转为大写的修饰器,注册为 'to_upper',模板中 {$name|to_upper}。
- 复杂业务逻辑:封装多参数处理(如格式化、拼接、计算),通过冒号参数传递配置项。
- 异步函数:模板渲染为同步过程,不建议在修饰器中执行阻塞 IO;可将耗时任务放入后台队列,修饰器仅返回占位符或缓存键。
调试方法与性能优化技巧
- 调试:在修饰器内记录日志或抛出异常;使用 DouView 的 force_compile 与 compile_check 快速重编测试。
- 性能:避免在修饰器中进行数据库查询或网络请求;使用缓存键与一次性计算;缩短修饰器链。
- 安全:对所有输入进行类型校验与白名单过滤;使用内置 escape 修饰器或全局转义;避免执行任意代码。
安全验证与输入过滤最佳实践
- 输入校验:在修饰器开头对参数类型与范围进行检查,拒绝非法输入。
- 输出转义:优先使用内置 escape 修饰器或全局 escapeHtml;避免自行拼接 HTML。
- 权限控制:修饰器不应直接访问敏感数据,应通过服务层或门面获取受控数据。
- 防注入:对动态生成的 SQL、命令或文件路径进行严格校验与白名单限制。