插件开发指南
本指南面向 BlogKit 插件开发者,介绍插件框架的能力、目录规范、清单字段与各开发专题。模板标签语法请同时参考 主题开发指南,开放 API 相关能力参考 API 文档。
一、插件能做什么
BlogKit 插件框架(2.0)提供以下能力,全部通过继承 PluginBase 基类获得:
- 钩子监听:订阅系统在业务流程中触发的钩子(文章发布、评论提交、文件上传等),在不修改核心代码的前提下介入业务逻辑;
- 挂载点注入:向后台模板的固定埋点(如后台
<head>、仪表盘快捷入口卡)输出自定义 HTML/CSS/JS; - 前台路由接管:在 plugin.json 中声明
routes,自定义 URL 由插件控制器响应,支持{param}路径参数; - 后台页面:声明
admin_routes注册自有后台页面,访问地址为admin.php?action=plugin.{slug}.{page},从「插件管理 → 插件详情 → 配置」进入; - 自有模板渲染:插件目录内
templates/下的模板由核心模板引擎渲染,可用全部模板标签; - 设置项:在 plugin.json 中声明
default_settings,系统自动生成配置表单并持久化; - 系统服务访问:数据库、缓存、日志、配置、路由等核心服务可直接获取;
- 插件间通信:调用其他插件的公开方法、向其他插件派发事件。
插件不占用后台侧边栏菜单:所有插件(含官方插件)的管理页统一从「插件管理 → 插件详情 → 配置」进入。
二、插件目录结构
一个插件是 plugins/ 下的一个独立目录,目录名必须与 slug 一致:
plugins/hello/
├── plugin.json # 插件清单(必需):名称、版本、路由、设置项声明
├── plugin.php # 入口文件(必需):负责引入主类文件
├── HelloPlugin.php # 插件主类(必需):类名为 PascalCase(slug) + Plugin
├── Admin/ # 后台控制器目录(按需)
│ └── HelloAdminController.php
├── Controllers/ # 前台路由控制器目录(按需)
├── templates/ # 插件模板目录(按需),由 Plugin::view 渲染
├── assets/ # 静态资源(CSS/JS,按需)
├── icon.png # 插件图标
├── thumbnail.png # 插件详情页缩略图
└── README.md # 说明文档(建议提供)主类命名规则:slug 按下划线分段、每段首字母大写后拼接,再加 Plugin 后缀。例如 hello_world 对应类名 HelloWorldPlugin;系统也兼容无 Plugin 后缀的类名。入口文件 plugin.php 只做一件事——引入主类文件。
附:PluginBase 基类方法速查
插件主类继承 PluginBase 后可直接使用以下方法(均为源码核实的方法签名):
| 方法 | 签名 | 用途 |
|---|---|---|
init | init() | 初始化入口,每次请求加载插件时执行,子类重写用于注册钩子 |
getInfo | getInfo() | 返回传入构造函数的插件信息数组 |
getManifest | getManifest() | 返回 plugin.json 解析结果(带内存缓存) |
addHook | addHook($hook, callable $callback, $priority = 10) | 注册钩子/挂载点订阅,优先级数值越小越先执行 |
adminUrl | adminUrl($page) | 生成插件后台页 URL:admin.php?action=plugin.{slug}.{page} |
view | view($tpl, $data = []) | 渲染插件 templates/ 目录下模板,$data 注入为模板变量 |
install | install() | 生命周期:安装时调用 |
activate | activate() | 生命周期:启用时调用 |
deactivate | deactivate() | 生命周期:停用时调用 |
uninstall | uninstall($keepData = false) | 生命周期:卸载时调用,$keepData 指示是否保留数据 |
callPlugin | callPlugin($slug, $method, ...$args) | 调用其他已启用插件的公开方法 |
triggerPluginEvent | triggerPluginEvent($slug, $event, $data = null) | 触发目标插件的 on{事件名} 方法 |
getPlugin | getPlugin($slug) | 获取指定已启用插件实例 |
getActivePlugins | getActivePlugins() | 获取全部已启用插件 |
getService | getService($serviceName) | 获取系统服务(database/config/log/cache/router/template/plugin) |
getDatabase | getDatabase() | 快捷获取数据库服务 |
getConfig / getLog / getCache / getRouter / getTemplate / getPluginService | 同上形式 | 快捷获取对应系统服务 |
log | log($level, $message, $context = []) | 按级别记录日志 |
debug / info / warning / error / critical | ($message, $context = []) | 各级别日志便捷方法 |
三、plugin.json 字段全表
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | 是 | 插件名称,2-50 个字符 |
slug | string | 是 | 插件标识,仅小写字母/数字/下划线/连字符,2-50 字符,必须与目录名一致 |
version | string | 是 | 插件版本,SemVer 格式(如 1.0.0) |
description | string | 是 | 插件描述,建议不超过 500 字符 |
author | string | 是 | 作者名称 |
author_url | string | 否 | 作者主页 URL(需为合法 URL) |
compatible_version | string | 否 | 声明兼容的主系统版本,SemVer 写法,^2.0.0 表示兼容 2.0.0 及以上、同一主版本内的系统 |
dependencies | array/object | 否 | 依赖的其他插件;依赖项可带 version 最低版本与 optional(可选依赖),安装时系统会校验 |
settings_page | boolean | 否 | 是否有配置页;声明为 true 时详情页出现「配置」入口 |
icon | string | 否 | 图标文件名(相对插件目录,如 icon.png) |
thumbnail | string | 否 | 缩略图文件名(相对插件目录) |
default_settings | object | 否 | 设置项声明,详见「设置项开发」一节 |
admin_routes | object | 否 | 后台页面路由表,键为 plugin.{slug}.{page},值为 控制器文件@方法名 |
routes | array | 否 | 前台路由接管列表,每项含 path/handler/method |
plugin_url | string | 否 | 插件主页 URL |
changelog | array | 否 | 更新记录,每条含 version/date |
四、快速开始:Hello 插件
下面完成一个最小可运行的插件:监听文章内容渲染钩子、向后台 admin.head 挂载点注入内容、提供一个设置项。
1. plugins/hello/plugin.json
{
"name": "Hello",
"slug": "hello",
"version": "1.0.0",
"settings_page": true,
"description": "最小示例插件:演示钩子监听、挂载点注入与设置项读写",
"author": "Your Name",
"author_url": "https://example.com",
"compatible_version": "^2.0.0",
"dependencies": [],
"icon": "icon.png",
"thumbnail": "thumbnail.png",
"default_settings": {
"signature": {
"type": "text",
"label": "文章尾部签名",
"value": "本文由 Hello 插件提供",
"help": "追加在每篇文章正文末尾的签名文字,留空则不追加"
}
}
}2. plugins/hello/plugin.php(入口文件)
<?php
// 入口文件:插件系统根据 slug 加载本文件,并实例化主类
// 类名规则:PascalCase(slug) + 'Plugin',即 hello -> HelloPlugin
require_once __DIR__ . '/HelloPlugin.php';3. plugins/hello/HelloPlugin.php(主类)
<?php
// 插件主类必须继承 PluginBase,才能获得钩子注册、服务访问等能力
class HelloPlugin extends PluginBase {
// init() 在每次请求加载插件时执行(仅启用状态的插件会被加载),
// 是注册钩子与挂载点订阅的唯一时机
public function init() {
// 注册钩子:文章正文渲染过滤器,回调返回处理后的内容
$this->addHook('article_content_render', [$this, 'appendSignature']);
// 订阅后台挂载点:admin.head 在后台所有页面 </head> 前执行,
// 回调返回的字符串会被直接输出到页面(也可在回调中直接 echo)
$this->addHook('admin.head', [$this, 'injectHead']);
}
// 钩子回调:$content 为文章正文 HTML,必须返回处理后的内容
public function appendSignature($content) {
// 读取本插件设置:getPluginConfig 返回「默认值 + 已保存值」合并结果
$settings = Plugin::getInstance()->getPluginConfig($this->plugin['slug']);
$signature = isset($settings['signature']['value'])
? $settings['signature']['value']
: (isset($settings['signature']) ? $settings['signature'] : '');
if ($signature === '' || $signature === null) {
return $content; // 未填写签名时不修改正文
}
// 追加签名段落(生产环境请对富文本内容做必要的转义处理)
return $content . '<p class="hello-signature">' . htmlspecialchars($signature) . '</p>';
}
// 挂载点回调:$context 为埋点处透传的上下文数组(admin.head 无上下文),
// 返回字符串由系统输出到挂载点位置;单个回调抛出异常不会影响页面渲染
public function injectHead($context = []) {
// 示例:注入一条元信息。实际开发中通常在此输出插件的 CSS/JS 引用
echo '<meta name="plugin" content="hello">';
}
}将目录放入 plugins/ 后,到「插件管理」安装并启用即可生效。
五、核心开发专题
5.1 钩子监听
通过 $this->addHook($hook, $callback, $priority = 10) 注册($priority 数值越小越先执行)。回调通过 Hook::trigger / Hook::triggerWithReturn / Hook::filter 三种方式被触发:普通触发拼接字符串返回值;带返回值触发收集全部回调返回值为数组;过滤器将数据依次传入各回调并以最终返回值替换。
以下是已在核心代码中验证触发的钩子清单(推荐监听这些钩子;Hook 类中还定义了更完整的常量清单,可按需监听):
| 钩子名 | 触发位置 | 参数 | 说明 |
|---|---|---|---|
system_init | API 入口引导阶段 | 无 | 系统初始化 |
api_dispatch | API 入口 | 无 | 返回值数组非空即视为插件接管本次 API 请求 |
article_content_render | 前台文章详情渲染 | 正文 HTML(过滤器) | 修改文章正文 |
page_content_render | 前台页面详情渲染 | 内容 HTML(过滤器) | 修改页面内容 |
front_profile_page | 个人中心各页渲染前 | 页面标识字符串 | 回调返回 true 表示接管本页渲染 |
front_interaction_action | 前台互动 AJAX 端点 | ['action' => 'like'/'favorite'/'follow'] | 返回 true 表示接管本次请求 |
comment_posted | 评论提交后 | comment_id/article_id/user_id/parent_id/auto_approve | 评论后续处理 |
profile_settings_deletion | 个人中心设置页 | ['user', 'template', 'action'] | 注入注销等功能变量 |
template_notification_count | 模板引擎变量注入 | 用户 ID | 返回通知未读数 |
template_plugin_flags | 模板引擎变量注入 | 无 | 返回插件可用性标记数组,注入为模板变量 |
article_create_before / article_create_after | 文章模型创建前后 | 文章数据 / 文章记录 | 文章创建 |
comment_add_before / comment_add_after | 评论模型添加前后 | 评论数据 / 评论记录 | 评论添加(回复为 comment_reply_* 系列) |
send_notification | 通知发送(多处) | 通知数据数组 | 自定义通知处理 |
upload_finished | 文件落盘后、生成文件信息前 | file_path/file_type/thumbnail | 图片压缩等后处理 |
file_upload_after | 上传完成 | 文件信息数组(含 name/path/relative_path/type) | 云存储同步等 |
article_add_before / article_add_after | 后台文章新增页渲染前后 | 无 | 后台编辑页注入(编辑页为 article_edit_before/after,参数含 article_id) |
article_save_before | 后台保存文章前 | ['data' => $_POST] | 保存前处理 |
page_add_before / page_add_after 等 | 后台页面增改 | 同文章系列(编辑含 page_id) | 页面编辑页注入 |
admin_header / admin_footer | 后台公共头尾模板 | 无 | 后台全局输出 |
plugin_admin_actions | 插件详情页操作区 | 插件记录数组 | 向详情页追加操作 |
plugin_admin_controller | 插件管理控制器内 | 无 | 插件扩展插件管理页 |
admin_config_collect_custom | 后台保存系统配置时 | $_POST 数组 | 返回 ['字段名' => 值] 注入配置 |
admin_user_list_field | 后台用户列表 | ['users' => [...]](过滤器) | 过滤用户列表字段 |
admin_user_detail_field | 后台用户详情 | ['user' => &$user] | 修改用户详情(引用传递) |
主题模板中还可通过 {plugin_hook name="钩子名"} 标签在任意位置触发钩子(无参数)。
5.2 挂载点注入
挂载点(slot)是后台模板中的精确埋点,通过 Plugin::slot(挂载点名, 上下文数组) 执行,订阅方式与钩子完全一致(addHook),回调返回的字符串(或可转字符串的对象/字符串数组)直接输出到页面。单个回调抛出的异常会被捕获并记录日志,不会中断页面渲染。
已验证的挂载点清单(共 11 个,覆盖 19 处模板埋点):
| 挂载点 | 埋点位置 | 上下文参数 |
|---|---|---|
admin.head | 后台所有页面 </head> 前 | 无 |
admin.footer | 后台所有页面 </body> 前 | 无 |
dashboard.quick.links | 后台仪表盘快捷入口卡区域 | 无 |
article.title.after | 后台文章新增/编辑页标题区之后 | `['mode' => 'add' |
article.editor.before | 后台文章编辑器区域之前 | 同上 |
article.panel.bottom | 后台文章侧栏面板底部 | 同上 |
article.form.after | 后台文章表单之后 | 同上 |
page.title.after | 后台独立页面标题区之后 | `['mode' => 'add' |
page.editor.before | 后台页面编辑器区域之前 | 同上 |
page.panel.bottom | 后台页面侧栏面板底部 | 同上 |
page.form.after | 后台页面表单之后 | 同上 |
注入示例(输出仪表盘快捷入口卡,使用 $this->adminUrl() 生成插件后台页链接):
public function init() {
$this->addHook('dashboard.quick.links', [$this, 'quickLink']);
}
public function quickLink($context = []) {
// adminUrl('index') 生成 admin.php?action=plugin.hello.index
$url = $this->adminUrl('index');
return '<a href="' . $url . '" class="plugin-quick-card">我的插件</a>';
}5.3 前台路由接管
在 plugin.json 中声明 routes,核心路由分发前会按「先注册先得」顺序匹配插件路由,命中即由插件控制器处理,未命中回落核心路由:
"routes": [
{
"path": "hello/{name}",
"handler": "Controllers/HelloController.php@show",
"method": "GET"
}
]path:相对站点根路径,支持{param}占位符(匹配除/外的任意字符),参数按出现顺序传给处理器方法;handler:控制器文件相对路径@方法名,控制器文件必须位于插件目录内;method:GET/POST等,*或留空匹配任意方法。
<?php
// plugins/hello/Controllers/HelloController.php
class HelloController {
// $name 由路由占位符 {name} 按顺序传入
public function show($name = '') {
// 渲染插件模板:模板根目录指向 plugins/hello/templates/,
// 模板内可使用全部模板标签(引擎约束与主题模板一致,如循环不可嵌套)
Plugin::getInstance()->view('hello', 'greet.html', [
'name' => htmlspecialchars($name),
]);
}
}5.4 后台页面开发
在 plugin.json 中声明 admin_routes(键为访问 action,值为 控制器文件@方法名,控制器文件相对插件根目录):
"admin_routes": {
"plugin.hello.index": "Admin/HelloAdminController.php@index"
}访问地址为 admin.php?action=plugin.hello.index;建议在插件内用 $this->adminUrl('index') 生成,避免手写。控制器为普通类(无需继承基类),方法内自行渲染页面:
<?php
// plugins/hello/Admin/HelloAdminController.php
class HelloAdminController {
public function index() {
// 读取插件设置并展示(getPluginConfig 返回默认结构合并已保存值)
$config = Plugin::getInstance()->getPluginConfig('hello');
// 方式一:直接引入插件目录内模板(同插件内传参可直接用局部变量)
$signature = $config['signature']['value'] ?? '';
include __DIR__ . '/../templates/admin_index.html';
// 方式二:走模板引擎渲染,Plugin::getInstance()->view('hello', 'admin_index.html', $data)
}
}控制器输出的内容会嵌入后台布局;若需要在 <head> 中加载插件专属 CSS/JS,订阅 admin.head 挂载点输出 <link>/<script> 标签即可(参考官方插件的做法,资源地址附带文件修改时间作为版本参数以刷新缓存)。
模板文件示例 plugins/hello/templates/admin_index.html:
<!-- 插件后台页面模板:可使用与主题模板一致的标签语法输出变量 -->
<div class="plugin-page">
<h2>Hello </h2>
<p>{$signature}</p>
</div>5.5 设置项
default_settings 中每一项的键为设置名,值结构支持:type(类型)、label(显示名)、value(默认值)、help(帮助文本)、attributes(HTML 属性,如 min/max/step/required/accept)、options(select/radio/checkbox 选项)、dependency(依赖其他设置项的条件显示)、tab(分组到 config_tabs 标签页)。
支持的类型:boolean、string/text、number、select、radio、checkbox、password、email、url、date、time、datetime、color、file、rich_text、hidden。
"default_settings": {
"enabled": {
"type": "boolean",
"label": "启用功能",
"value": true,
"help": "关闭后插件功能整体停用"
},
"api_key": {
"type": "password",
"label": "接口密钥",
"value": "",
"attributes": { "required": true }
},
"mode": {
"type": "select",
"label": "工作模式",
"value": "auto",
"options": { "auto": "自动", "manual": "手动" }
}
}读写 API(均在 Plugin 管理器上,公开方法):
Plugin::getInstance()->getPluginConfig($slug):读取设置,返回默认结构合并数据库已保存值的结果;Plugin::getInstance()->savePluginConfig($slug, $config):保存设置(boolean项的取消勾选由系统自动补齐为false)。
设置保存在插件表 settings 字段(JSON)。插件内部建议封装一个「默认值 + 已保存值」合并读取方法,参考官方插件的写法。
卸载数据规则:卸载默认保留数据。插件可在 default_settings 中声明布尔项 keep_data_on_uninstall(卸载确认弹窗未显式选择时,系统读取该设置作为是否保留数据的依据),并在 uninstall($keepData) 中按该参数决定是否删除自有数据表与配置。
六、数据库操作
插件通过基类 getDatabase() 获取系统数据库服务(亦可 Database::getInstance()),主要方法:
| 方法 | 说明 |
|---|---|
table($table) | 返回带系统前缀的完整表名(拼接 SQL 时必须使用,避免硬编码前缀) |
query($sql, $params, $cache = false, $expire = 3600) | 执行 SQL,$params 为绑定参数数组(预编译占位符 ?),可缓存结果 |
fetch($sql, $params, ...) | 查询单行 |
fetchAll($sql, $params, ...) | 查询多行 |
insert($table, $data) | 插入一行,$data 为关联数组 |
update($table, $data, $where) | 更新,$where 为条件数组 |
delete($table, $where) | 删除 |
beginTransaction() / commit() / rollback() | 事务控制 |
系统不为插件提供独立的建表迁移机制:插件如需自有数据表,在 install() 生命周期方法中通过上述 API 自行建表与写入种子数据(保持幂等,重复安装不覆盖已有数据),并在 uninstall($keepData) 中根据 $keepData 决定是否清理。始终使用参数绑定,禁止字符串拼接 SQL。
七、访问控制与权限
- 前台路由:由插件控制器自行处理会话与权限;
- 后台插件页面(
plugin.{slug}.{page})采用两级控制,均由后台权限中间件在分发前完成:- 启用即放行:插件处于启用状态且目标页面已在
admin_routes注册即放行;插件停用/卸载或路由未注册时返回 403(停用插件的分发还会被路由分发器以 404 兜底); - 角色授权名单(ACL):站长可在插件详情页「访问授权」中配置可访问角色;未配置时对所有可登录后台的角色缺省放行,配置后仅名单内角色可访问,超级管理员(角色 1)始终放行;
- 启用即放行:插件处于启用状态且目标页面已在
- 后台其余功能页沿用系统基于角色的权限模型(权限代码 + 角色授权),插件不写入权限表。
八、生命周期
插件状态存于数据库(未安装/停用/启用/已卸载),各阶段系统行为与插件可响应的方法:
| 阶段 | 系统行为 | 插件可响应 |
|---|---|---|
| 发现 | 扫描 plugins/ 目录下含 plugin.json 的目录,校验 name/slug/version/description/author 完整性 | — |
| 安装 | 依赖与版本兼容校验;注册到数据库(默认停用);实例化插件 | install():建表、写入种子配置等(建议幂等) |
| 启用 | 实例化插件后更新状态为启用 | activate():启用初始化 |
| 每次请求 | 加载所有启用插件,实例化并调用 | __construct() 与 init():注册钩子/挂载点订阅(此时才可依赖启用状态) |
| 停用 | 实例化插件后更新状态为停用 | deactivate():清理临时状态 |
| 卸载 | 二次确认弹窗;实例化插件并传入是否保留数据;状态置为已卸载;清除该插件的后台页面授权名单 | uninstall($keepData):按 $keepData 清理或保留数据 |
注意:init() 仅在插件启用时执行,停用即完全退出,主系统行为不受影响;不要把必须在安装期完成的工作放进 init()。
九、调试技巧
- 开发者工具箱插件(官方插件):提供钩子管理与测试、钩子执行日志查看/导出/清除、页面调试面板与调试参数设置。安装启用后,从「插件管理 → 插件详情 → 配置」进入,可查看全部钩子定义、向任意钩子注入测试回调、按时间线排查钩子执行耗时与返回值;
- 调试配置:系统设置中的调试分组可开启调试模式、错误显示与慢查询日志,对应
debug_*系列配置键; - 日志:插件内可用基类
debug()/info()/warning()/error()方法记录日志,便于线上排查; - 卸载残留排查:插件卸载默认保留数据,重装同 slug 插件不会静默继承旧的后台授权名单(系统会清除),但插件自有数据需在
uninstall()中自行处理。
十、打包与发布
- 目录即包:插件目录本身就是发布单元,zip 包根级应直接包含以 slug 命名的文件夹,文件夹内首层即为
plugin.json与入口文件,不要嵌套多余目录; - 发布前自检:核对 plugin.json 必填字段、版本号 SemVer 格式、slug 与目录名一致、主类继承
PluginBase、无eval()等高风险调用;系统内置的打包校验工具(PluginValidator)可对插件目录执行结构、清单与安全检查,并输出错误与警告摘要; - 图标与缩略图:
icon.png用于列表展示,thumbnail.png用于详情页封面,均放置在插件根目录并在清单中声明; - README:随包附
README.md,说明功能、配置项含义与卸载数据策略; - 应用市场上架:打包 zip 后提交到 BlogKit 应用市场,通过官方审核后即可被站长搜索安装;上架信息(名称、版本、描述、图标)取自 plugin.json,更新插件时递增
version并保持 changelog 同步。