本文档是主题和插件共享的稳定事件面。插件使用 $ctx->on() / $ctx->filter() 注册;主题业务模块同样使用主题上下文注册。停用扩展时,系统会按扩展标签移除其监听器。
Action 事件
Action 监听器不能修改核心结果,异常会被隔离并记录日志。
事件 | 参数 | 触发时机 |
|---|---|---|
|
| 新文章写入后 |
|
| 已有文章更新后 |
|
| 每次保存文章后(新建与更新都触发) |
|
| 文章移入回收站后 |
|
| 文章永久删除后 |
|
| 文章首次变为已发布(含批量/定时) |
|
| 点赞或收藏切换成功后 |
| 同上 | 收藏切换成功后,作为语义别名额外触发 |
|
| 访客评论写入后 |
|
| 评论审核状态改变后 |
|
| 评论删除后 |
|
| 管理员回复写入后 |
|
| 用户注册成功后 |
|
| 用户登录成功后 |
|
| 用户退出成功后 |
|
| 用户被删除、其内容已转移后 |
| 微语 payload | 微语发布后 |
| 微语 payload | 微语删除后 |
| 微语 payload + | 每次保存微语后(含草稿与更新) |
|
| 前台 |
|
| 前台侧边栏渲染时 |
|
| 前台页脚渲染时 |
注入类 action 的用法
head_inject / sidebar_inject / footer_inject 是直接输出型 action——监听器用 print / echo 输出 HTML,核心不收集返回值:
$ctx->on('footer_inject', static function (array $context = []): void {
echo '<p>来自插件的页脚内容</p>';
});核心自带三个渲染器,把插件在 manifest 中声明为 head / sidebar / footer 的注入点转成 HTML。因此想让注入生效,插件仍必须在 plugin.json 的 injects 中声明对应点位。主题模板里已有这三个 do_action 调用,主题通常不需要自己监听。
优先级 0 表示核心渲染器先跑;插件的常规监听器(默认优先级 10)会在其后执行。
文章类 postPayload 的字段为:id、title、slug、status、publishedAt、categoryId、externalUrl、isPinned、categoryPinned。id 和 categoryId 使用字符串或 null,避免扩展收到数据库整数类型差异。
Filter 事件
Filter 监听器必须返回过滤后的值;异常会保留上一个值并继续执行后续监听器。
事件 | 输入值 | 额外参数 | 返回值 |
|---|---|---|---|
| 编辑器提供者映射 |
| 以编辑器 ID 为键的配置,含 plugin、label、format、scripts、styles;仅接纳已启用插件 assets/ 下的静态资源 |
| 保存字段数组 |
| 字段数组,或返回 |
| 正文原文 |
| 与输入相同格式的正文字符串 |
|
| 评论提交上下文 | 同形状数组,或 |
| 注册决策数组 | 注册字段 | 同形状数组,或 |
| 登录决策数组 | 登录字段 | 同形状数组,或 |
| 模板候选名数组 |
| 过滤后的模板名数组 |
| 微语保存字段数组 |
| 字段数组,或返回 |
| 渲染后的 HTML |
| HTML 字符串 |
| 设置值(字符串) |
| 标量;非标量时回退原值 |
| 文件负载数组 | 无 | 同形状数组 |
| OG / canonical 元信息数组 | 当前页面上下文 | 元信息数组 |
|
|
| 非空字符串 URL 表示已接管;否则核心走本地存储 |
| 上一个值 |
| 原样返回 |
|
|
| 追加到公开条件的 SQL 片段;空串表示不追加 |
|
| 文章行数组 | 决策型:返回 |
公开可见性过滤器的用法
posts_public_visibility_sql 与 post_is_public 是“文章能否被未授权访客看到”这一判定的两半。核心在首页、分类、标签、归档、搜索、RSS、sitemap、开放 API 和用户中心统一使用 Posts::publicWhere(),单篇详情与互动接口则使用 Posts::isPublic()。
$ctx->filter('posts_public_visibility_sql', static function (mixed $sql, array $args): string {
// 例如把“登录会员可见”的文章排除出公开列表
return "p.member_only = 0";
});
$ctx->filter('post_is_public', static function (mixed $public, array $post): bool {
return $post['member_only'] ?? 0 ? false : ($public === true);
});posts_public_visibility_sql的返回片段会被包进括号拼进WHERE,必须使用$args['alias']前缀(别名由调用方传入,可为空);不要返回完整WHERE或带分号的语句。include_owned_private为false时表示调用方(RSS、sitemap)只接受完全公开内容,可以据此收紧条件。post_is_public是决策型过滤器:false会短路后续过滤器,不能再被其他扩展放行;返回非true的标量同样按不可见处理。两者必须成对实现。只改 SQL 会出现“列表看不到、直链能打开”;只改单篇判定会出现“列表看得到、点进去 404”。
内容与设置类过滤器
markdown_html —— 在 Markdown 渲染成 HTML 之后执行,可做外链处理、代码块增强等:
$ctx->filter('markdown_html', static function (string $html, array $args): string {
// $args['safeMode'] 为 true 时表示未开启原始 HTML
return $html;
});与 filter_post_content 的区别:后者作用于正文原文(渲染前),本钩子作用于 HTML(渲染后)。正文格式由 contentFormat 标明,扩展应保留该格式,不要把 HTML 当 Markdown 返回。
theme_setting_value —— 每次 theme_value() 取值时执行,可用于按条件覆盖主题设置:
// 注意:第 2、3 个参数是位置参数,不是数组
$ctx->filter('theme_setting_value', static function (mixed $value, string $key, mixed $default): mixed {
if ($key === 'accent_color') { return '#b03a2e'; }
return $value; // 非标量返回值会被忽略并回退原值
});upload_file —— 上传内容写入本地或云存储之前执行,适合水印、压缩、病毒扫描。
负载与 upload_store_to_cloud 同形状(buffer / ext / mime / originalName / size / width / height):
$ctx->filter('upload_file', static function (array $file): array {
if (($file['mime'] ?? '') === 'image/jpeg') {
$file['buffer'] = MyImage::watermark($file['buffer']);
$file['size'] = strlen($file['buffer']);
}
return $file; // 必须返回数组;buffer 为空时核心会用原内容
});云存储过滤器的用法
upload_store_to_cloud 是接管型过滤器:默认值是 null,扩展返回非空字符串即表示
由自己处理,核心不再走本地存储。
$ctx->filter('upload_store_to_cloud', static function (mixed $prev, array $file): mixed {
if (is_string($prev) && $prev !== '') { return $prev; } // 已被其他扩展接管
$url = MyCloud::put($file['buffer'] ?? '', $file['ext'] ?? '');
return is_string($url) && $url !== '' ? $url : $prev; // 失败则交回核心走本地存储
});注意:第一个参数是上一个过滤器的结果,$file 是第二个参数。
$file 提供的是文件内容 buffer,不是路径:
键 | 说明 |
|---|---|
| 处理后的文件二进制内容 |
| 扩展名(不含点) |
| MIME 类型 |
| 用户上传时的原始文件名 |
| 字节数 |
| 图片尺寸;非图片为 |
失败时务必返回 $prev,否则会让后续扩展无法介入。
upload_delete_from_cloud 只是通知:核心把 $prev 原样传递,扩展在监听器里执行删除,
返回 $prev 即可。它不改变控制流,因此不能用来阻止删除。
frontend_meta 用于前台 <head> 的 OG / canonical 输出:输入是控制器算好的元信息数组,
主题或插件可覆盖标题、描述、图片等字段。
微语发布/删除分别触发 after_micro_publish、after_micro_delete,payload 至少包含 id、content、status、publishedAt、updatedAt、isPinned、isPrivate、authorId、media。微语是核心内容类型,插件和主题可以围绕它扩展,但不需要检测插件是否安装。
before_micro_save 的 input 是后台表单提交的原始数组;after_micro_save 在每次保存(含草稿与更新)后触发,payload 与 after_micro_publish 同构,额外带 created 与 input。扩展通过 micro_editor 注入点加自己的表单字段时,核心不认识这些键,需在 after_micro_save 里从 input 读出并写入插件表。
before_post_save 只允许修改标题、别名、摘要、正文和外链等受控字段;后台编辑器和 Markdown 导入都会触发它,核心会在写入前重新校验长度与别名冲突。filter_post_content 是一个简单的正文变换钩子:输入和输出都是 Markdown 字符串,详情页渲染前执行,不参与保存拒绝,也不参与编辑器实时预览。返回非字符串时核心会记录日志并继续使用原文。结果随后交给 Markdown 渲染器,扩展不应把它当作模板 HTML 注入接口。
theme_template_candidates 只影响模板候选顺序。文章详情依次查找所选 post-template-{name}.php、post-{category_slug}.php、post.php;过滤器返回的名称会再次经过模板白名单校验,非法项不会参与文件查找。
上述四个决策过滤器的 allowed=false 都在核心写入/认证前消费并返回受控错误状态;四者都兼容直接返回 false,且拒绝结果会短路后续决策过滤器,不能被后续扩展覆盖。过滤器异常由钩子引擎隔离,异常时保留上一个值,不会绕过核心校验。
版本与兼容性
事件名称和参数键按本文档维护。新增字段只会向后兼容地追加;删除或改变字段语义需要提升扩展 API 版本并在变更日志中说明。监听器应容忍未知字段和 null 值。
附录:完整钩子索引
按类型速查。本表是文档完整性的基准——核心新增钩子时必须同步登记。
Action(22 个)
钩子 | 载荷 | 类型 |
|---|---|---|
|
| 通知 |
|
| 通知 |
|
| 通知 |
|
| 通知 |
|
| 通知 |
|
| 通知 |
|
| 通知 |
| 同上 | 通知(别名) |
|
| 通知 |
|
| 通知 |
|
| 通知 |
|
| 通知 |
|
| 通知 |
|
| 通知 |
|
| 通知 |
|
| 通知 |
| 微语 payload | 通知 |
| 微语 payload | 通知 |
| 微语 payload + | 通知 |
|
| 直接输出( |
head_inject/sidebar_inject/footer_inject三个共用一条"直接输出"契约,故 Action 表共 22 个钩子名。
Filter(16 个)
钩子 | 首个参数 | 返回值 |
|---|---|---|
| 编辑器提供者映射 | 编辑器配置映射 |
| 保存字段数组 | 字段数组 / 拒绝 |
| 微语字段数组 | 字段数组 / 拒绝 |
| 决策数组 | 决策数组 / 拒绝 |
| 决策数组 | 决策数组 / 拒绝 |
| 决策数组 | 决策数组 / 拒绝 |
| 正文原文 | 与 |
| 渲染后 HTML | HTML 字符串(渲染后) |
| 模板候选数组 | 模板名数组 |
| 设置值 | 标量(非标量回退) |
| 元信息数组 | 元信息数组 |
| 文件负载数组 | 同形状数组 |
|
| 非空 URL / 原值 |
| 上一个值 | 原值(仅通知) |
|
| SQL 片段 / 空串 |
|
| 布尔(决策型) |
上传管线顺序
一次上传依次经过三个钩子:
upload_file 改写内容(水印/压缩/扫描)—— 可选
↓
upload_store_to_cloud 接管存储,返回云端 URL —— 可选
↓(未接管时)
核心本地存储 storeLocal()
↓
upload_delete_from_cloud 删除时通知云存储 —— 仅通知公开可见性两段式判定
列表 / 归档 / RSS / sitemap / 开放 API / 用户中心
→ Posts::publicWhere() → posts_public_visibility_sql(追加 SQL 片段)
单篇详情 / 评论 / 点赞收藏
→ Posts::isPublic() → post_is_public(决策型,false 即不可读)两个钩子必须成对实现,否则会出现“列表看不到、直链能打开”或“列表看得到、点进去 404”。
决策类过滤器(5 个)
before_post_save、before_micro_save、before_comment_submit、before_register、before_login
共享同一套拒绝协议:返回 false 或数组内 allowed=false 即拒绝,并短路后续过滤器,
后面的扩展无法覆盖该拒绝。