插件开发
CnLog 采用钩子驱动的插件架构,通过 HookManager 管理动作钩子(Action)和过滤器钩子(Filter),插件以独立目录形式存在,通过 plugin.json 声明元信息,hooks.json 注册钩子回调。
一、插件体系概述
1.1 核心机制
| 机制 | 说明 | 相关函数 |
|---|---|---|
| 动作钩子 | 在特定时机执行回调,不修改数据 | doAction() / addAction() / removeAction() |
| 过滤器钩子 | 拦截并修改数据,返回修改后的值 | applyFilter() / addFilter() / removeFilter() |
| 生命周期回调 | 激活/停用/卸载时执行特定逻辑 | 在 plugin.json 中声明 callback 函数 |
| 资源注入 | 通过钩子注入 CSS/JS 到页面 | index_head / footer_after 等钩子 |
1.2 插件加载流程
系统启动 → HookManager::init()
→ 从 app_extensions 表查询 is_enabled = 1 的插件(表前缀可在安装时自定义)
→ 遍历每个已启用插件:
1. require_once plugin.php(加载主文件)
2. 读取 hooks.json
3. 注册所有 action/filter 钩子到 HookManager
→ 钩子注册完成,等待触发
页面请求 → 执行 doAction('xxx') / applyFilter('xxx')
→ HookManager 按优先级排序执行所有注册的回调
1.3 核心全局函数
// 注册动作钩子 addAction(string $hook, callable $callback, int $priority = 10); // 触发动作钩子 doAction(string $hook, ...$args); // 注册过滤器钩子 addFilter(string $hook, callable $callback, int $priority = 10); // 应用过滤器 $value = applyFilter(string $hook, $value, ...$args); // 移除钩子 removeAction(string $hook, callable $callback); removeFilter(string $hook, callable $callback); // 检查钩子是否有注册 hasAction(string $hook): bool; hasFilter(string $hook): bool;
二、目录结构规范
your_plugin/
├── plugin.json # 【必须】插件元信息声明
├── hooks.json # 【必须】钩子注册配置
├── plugin.php # 【必须】插件主入口文件
├── admin_settings.php # 【可选】后台设置页面模板
├── README.md # 【推荐】插件说明文档
├── config/
│ ├── config.default.php # 【推荐】默认配置
│ └── config.php # 【可选】运行时配置(自动生成)
├── includes/
│ ├── YourClass.php # 【推荐】核心业务类
│ └── helpers.php # 【可选】辅助函数
├── migrations/
│ └── 001_init.sql # 【可选】数据库迁移脚本
├── templates/
│ └── email_template.html # 【可选】模板文件
├── assets/
│ ├── css/
│ ├── js/
│ └── images/
└── languages/
└── zh_CN.php # 【可选】语言包
| 文件/目录 | 必需性 | 说明 |
|---|---|---|
plugin.json | 必须 | 插件元信息,包含名称、版本、作者、生命周期回调等 |
hooks.json | 必须 | 钩子注册配置,声明插件监听的所有钩子 |
plugin.php | 必须 | 插件主入口,包含回调函数和业务逻辑 |
admin_settings.php | 可选 | 后台设置页面,has_settings=true 时生效 |
config/ | 推荐 | 配置文件目录,默认配置与运行配置分离 |
includes/ | 推荐 | 核心类和辅助函数目录 |
migrations/ | 可选 | SQL 数据库迁移脚本 |
templates/ | 可选 | 插件自有模板文件 |
assets/ | 可选 | 静态资源(CSS/JS/图片) |
三、plugin.json 规范
plugin.json 是插件的元信息声明文件,系统通过它识别插件基本信息和生命周期回调。
3.1 完整示例
{
"name": "邮件通知插件",
"slug": "email_notification",
"version": "1.0.0",
"description": "为用户注册、订单支付等事件发送 SMTP 邮件通知。",
"author": "CNLOG Market",
"author_url": "https://www.cnlog.cc",
"icon": "fa-envelope",
"has_settings": true,
"php_min_version": "7.4",
"dependencies": [],
"activation": {
"sql_file": "install.sql",
"callback": "email_notification_activate"
},
"deactivation": {
"callback": "email_notification_deactivate"
},
"uninstall": {
"callback": "email_notification_uninstall",
"drop_tables": []
}
}
3.2 字段说明
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
name | string | 是 | 插件显示名称 |
slug | string | 是 | 插件唯一标识,小写+下划线,与目录名一致 |
version | string | 是 | 版本号,语义化版本(如 1.0.0) |
description | string | 是 | 插件功能描述 |
author | string | 是 | 作者名称 |
author_url | string | 否 | 作者主页 URL |
icon | string | 否 | Font Awesome 图标类名 |
has_settings | bool | 否 | 是否有后台设置页(默认 false) |
php_min_version | string | 否 | 最低 PHP 版本要求 |
dependencies | array | 否 | 依赖的其他插件 slug 列表 |
activation | object | 否 | 激活配置:sql_file, callback |
deactivation | object | 否 | 停用配置:callback |
uninstall | object | 否 | 卸载配置:callback, drop_tables |
四、hooks.json 规范
hooks.json 声明插件监听的所有钩子,系统启动时自动注册。
4.1 完整示例
[
{
"hook": "user_registered",
"type": "action",
"priority": 10,
"callback": "email_notification_on_user_registered"
},
{
"hook": "order_paid",
"type": "action",
"priority": 10,
"callback": "email_notification_on_order_paid"
},
{
"hook": "the_content",
"type": "filter",
"priority": 10,
"callback": "my_plugin_filter_content"
}
]
4.2 字段说明
| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
hook | string | 是 | 钩子名称 |
type | string | 是 | 钩子类型:action 或 filter |
priority | int | 否 | 优先级,数字越小越先执行,默认 10 |
callback | string | 是 | 回调函数名(必须在 plugin.php 中定义) |
五、plugin.php 入口文件
plugin.php 是插件的主入口文件,系统自动加载,必须包含所有在 hooks.json 中声明的回调函数。
5.1 标准结构
<?php
// 安全检查:禁止直接访问
if (!defined('CNLOG_ROOT') && !defined('ROOT_PATH')) {
exit('Access denied');
}
// 加载依赖文件
require_once __DIR__ . '/includes/YourClass.php';
// ========== 生命周期回调 ==========
function your_plugin_activate() {
// 激活时执行:创建配置、初始化数据等
$configFile = __DIR__ . '/config/config.php';
if (!file_exists($configFile)) {
copy(__DIR__ . '/config/config.default.php', $configFile);
}
return ['success' => true, 'message' => '插件已激活'];
}
function your_plugin_deactivate() {
// 停用时执行:清理缓存等(一般保留数据)
return ['success' => true, 'message' => '插件已停用'];
}
function your_plugin_uninstall() {
// 卸载时执行:删除配置、数据表等
$configFile = __DIR__ . '/config/config.php';
if (file_exists($configFile)) {
unlink($configFile);
}
return ['success' => true, 'message' => '插件已卸载', 'deleted_tables' => []];
}
// ========== 配置读写 ==========
function your_plugin_get_config() {
$configFile = __DIR__ . '/config/config.php';
if (file_exists($configFile)) {
return require $configFile;
}
return require __DIR__ . '/config/config.default.php';
}
function your_plugin_save_settings(array $settings) {
$config = your_plugin_get_config();
$validKeys = ['enabled', 'option1', 'option2'];
foreach ($validKeys as $key) {
if (isset($settings[$key])) {
$config[$key] = $settings[$key];
}
}
$configFile = __DIR__ . '/config/config.php';
$content = "<?php\nreturn " . var_export($config, true) . ";\n";
if (file_put_contents($configFile, $content)) {
return ['success' => true, 'message' => '设置保存成功'];
}
return ['success' => false, 'message' => '保存失败'];
}
// ========== 钩子回调函数 ==========
function your_plugin_on_user_registered(int $userId, string $email): void {
// 处理用户注册事件
$config = your_plugin_get_config();
if (empty($config['enabled'])) return;
// 业务逻辑...
}
function your_plugin_filter_content(string $content): string {
// 修改文章内容(过滤器钩子必须返回值)
return $content . '<p>附加内容</p>';
}
5.2 函数命名约定
所有全局函数必须以 {插件slug}_ 为前缀,避免与其他插件或内核冲突:
| 类型 | 命名格式 | 示例 |
|---|---|---|
| 激活回调 | {slug}_activate | email_notification_activate |
| 停用回调 | {slug}_deactivate | email_notification_deactivate |
| 卸载回调 | {slug}_uninstall | email_notification_uninstall |
| 配置读取 | {slug}_get_config | email_notification_get_config |
| 保存设置 | {slug}_save_settings | email_notification_save_settings |
| 钩子回调 | {slug}_on_{事件} | email_notification_on_order_paid |
| 工具类 | {slug}_{描述} | email_notification_send_mail |
六、后台设置页
当 plugin.json 中 has_settings 为 true 时,系统会加载 admin_settings.php 作为设置页面。
6.1 页面模板结构
<?php
$config = your_plugin_get_config();
?>
<style>
.your-plugin-settings { padding: 0; }
.your-plugin-settings .form-group { margin-bottom: 16px; }
.your-plugin-settings label { display: block; margin-bottom: 8px; }
.your-plugin-settings input,
.your-plugin-settings select { width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; }
</style>
<div class="your-plugin-settings">
<div class="form-group">
<label>启用功能</label>
<input type="checkbox" name="enabled" value="1"
<?php echo !empty($config['enabled']) ? 'checked' : ''; ?>>
</div>
<div class="form-group">
<label>选项名称</label>
<input type="text" name="option_name"
value="<?php echo htmlspecialchars($config['option_name'] ?? ''); ?>">
</div>
<div class="form-group">
<label>选择项</label>
<select name="select_option">
<option value="a" <?php echo ($config['select_option'] ?? '') === 'a' ? 'selected' : ''; ?>>选项A</option>
<option value="b" <?php echo ($config['select_option'] ?? '') === 'b' ? 'selected' : ''; ?>>选项B</option>
</select>
</div>
</div>
6.2 保存机制
系统自动处理表单提交,调用 {slug}_save_settings() 函数保存设置。表单字段名直接对应配置数组的键。
七、配置系统
7.1 配置文件格式
使用 PHP return array 格式,相比 JSON 支持注释和更丰富的数据类型:
<?php
/**
* 邮件通知插件默认配置
*/
return [
'enabled' => 0,
'host' => 'smtp.example.com',
'port' => 587,
'secure' => 'tls',
'notify' => [
'user_register' => 1,
'order_paid' => 1,
],
];
7.2 配置读取流程
读取配置
├── 优先读取 config/config.php(用户自定义配置)
└── 不存在则读取 config/config.default.php(默认配置)
保存配置
├── 读取当前配置(合并默认值)
├── 校验并更新指定字段
└── 写入 config/config.php(PHP return array 格式)
7.3 配置设计原则
- 默认值完备:config.default.php 必须包含所有配置项的默认值
- 白名单保存:save_settings 只更新预定义的 validKeys 列表中的字段
- 类型明确:开关用 int(0/1),数字用 int/float,避免字符串类型混乱
- 分组清晰:相关配置用子数组分组(如 notify 子数组存通知开关)
- 向后兼容:新增配置项时,读取函数需处理键不存在的情况(?? 运算符)
八、生命周期回调详解
8.1 激活回调
管理员点击「激活」时执行,常用于:创建默认配置、初始化数据表、创建目录等。
function your_plugin_activate() {
// 1. 复制默认配置
$configFile = __DIR__ . '/config/config.php';
if (!file_exists($configFile)) {
copy(__DIR__ . '/config/config.default.php', $configFile);
}
// 2. 创建数据表(如有需要,也可在 SQL 文件中定义)
// $db = Database::getInstance();
// $db->execute("CREATE TABLE IF NOT EXISTS ...");
// 3. 创建必要目录
$uploadDir = __DIR__ . '/uploads';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
return ['success' => true, 'message' => '插件已激活'];
}
8.2 停用回调
管理员点击「停用」时执行,一般只清理运行时缓存,不删除数据。
function your_plugin_deactivate() {
// 清理缓存
// Cache::delete('your_plugin_cache_key');
return ['success' => true, 'message' => '插件已停用'];
}
8.3 卸载回调
管理员点击「卸载」时执行,必须清理所有插件数据:配置文件、数据表、上传文件等。
function your_plugin_uninstall() {
$deletedTables = [];
$errors = [];
// 1. 删除配置文件
$configFile = __DIR__ . '/config/config.php';
if (file_exists($configFile)) {
unlink($configFile);
}
// 2. 删除数据表(也可在 plugin.json 的 drop_tables 中声明)
// $db = Database::getInstance();
// $db->execute("DROP TABLE IF EXISTS your_plugin_table");
// $deletedTables[] = 'your_plugin_table';
// 3. 删除上传目录
// $uploadDir = __DIR__ . '/uploads';
// if (is_dir($uploadDir)) {
// array_map('unlink', glob($uploadDir . '/*'));
// rmdir($uploadDir);
// }
return [
'success' => empty($errors),
'message' => empty($errors) ? '插件已卸载' : '卸载完成,存在错误',
'deleted_tables' => $deletedTables,
'errors' => $errors,
];
}
九、安全规范
9.1 文件访问保护
| 正确写法 | 错误写法 |
|---|---|
if (!defined('ROOT_PATH')) {
exit('Access denied');
} |
// 没有安全检查,直接执行 // 可被直接 URL 访问 |
9.2 XSS 防护
| 正确写法 | 错误写法 |
|---|---|
echo htmlspecialchars(
$config['name'] ?? ''
); |
echo $config['name']; |
9.3 SQL 注入防护
| 正确写法 | 错误写法 |
|---|---|
$db->fetchAll(
"SELECT * FROM table WHERE id = ?",
[$id]
); |
$db->fetchAll(
"SELECT * FROM table WHERE id = $id"
); |
9.4 CSRF 防护
后台设置页面的表单提交由系统自动处理 CSRF 验证,插件无需额外处理。如自定义前端表单,需自行添加 CSRF Token。
9.5 文件操作安全
| 正确写法 | 错误写法 |
|---|---|
$file = __DIR__ . '/config/'
. basename($filename);
if (is_file($file)) { ... } |
$file = $_GET['file']; unlink($file); |
十、前端资源注入
插件可通过模板钩子向前端页面注入 CSS 和 JS。
10.1 注入 CSS
// 在 hooks.json 中注册 index_head 钩子
// { "hook": "index_head", "type": "action", "callback": "your_plugin_enqueue_styles" }
function your_plugin_enqueue_styles() {
$cssUrl = '/plugins/your_plugin/assets/css/style.css';
echo '<link rel="stylesheet" href="' . $cssUrl . '?v=1.0">';
}
10.2 注入 JS
// 在 hooks.json 中注册 footer_after 钩子
// { "hook": "footer_after", "type": "action", "callback": "your_plugin_enqueue_scripts" }
function your_plugin_enqueue_scripts() {
$jsUrl = '/plugins/your_plugin/assets/js/main.js';
echo '<script src="' . $jsUrl . '?v=1.0"></script>';
}
10.3 常用注入钩子
| 钩子 | 位置 | 用途 |
|---|---|---|
index_head | </head> 前 | 注入 CSS、meta 标签 |
footer_after | </footer> 后 | 注入 JS 脚本 |
header_after | </header> 后 | 页面顶部横幅、公告 |
footer_before | <footer> 前 | 页脚上方内容 |
十一、常用钩子参考
11.1 用户相关
| 钩子名 | 类型 | 参数 | 说明 |
|---|---|---|---|
user_registered | action | $userId, $email, $username | 用户注册成功后 |
email_verification_request | action | $userId, $email, $token | 请求邮箱验证时 |
password_reset_request | action | $userId, $email, $token | 请求重置密码时 |
11.2 订单相关
| 钩子名 | 类型 | 参数 | 说明 |
|---|---|---|---|
order_paid | action | $orderId, $userId, $amount, $email | 订单支付成功后 |
admin_notify_new_order | action | $orderId, $userId, $amount, $username, $email | 新订单管理员通知 |
11.3 评价相关
| 钩子名 | 类型 | 参数 | 说明 |
|---|---|---|---|
review_submitted | action | $reviewId, $userId, $appId, $email | 评价提交后 |
admin_notify_new_review | action | $reviewId, $userId, $appId, $username, $email | 新评价管理员通知 |
十二、调试技巧
12.1 钩子调试
// 查看某个钩子已注册的所有回调
$hooks = HookManager::getActions('hook_name');
// 临时输出所有已注册钩子(调试用)
// HookManager::debugDump();
12.2 错误日志
// 使用系统 error_log 记录调试信息
error_log('[your_plugin] 调试信息: ' . $var);
// 更规范的写法
function your_plugin_log(string $message): void {
error_log('[your_plugin] ' . $message);
}
12.3 常见问题排查
| 问题 | 可能原因 | 排查方法 |
|---|---|---|
| 钩子不生效 | hooks.json 格式错误或函数未定义 | 检查 JSON 语法、确认回调函数存在 |
| 激活失败 | 回调函数返回格式不对 | 检查 activate 函数返回数组结构 |
| 设置保存无效 | 字段不在 validKeys 中 | 检查 save_settings 中的白名单 |
| CSS/JS 不加载 | 钩子未注册或路径错误 | 检查 hooks.json 和资源 URL |
| 数据库操作报错 | SQL 语法或参数错误 | 使用参数化查询,检查字段名 |
十三、发布清单
发布插件前,请逐项检查:
- plugin.json 信息完整且正确(name, slug, version, author)
- hooks.json 中声明的所有回调函数在 plugin.php 中都有定义
- 所有 PHP 文件开头有安全检查(defined ROOT_PATH / CNLOG_ROOT)
- 函数名、类名都以插件 slug 为前缀,无命名冲突
- 输出用户内容时使用 htmlspecialchars() 转义
- 数据库操作使用参数化查询,无 SQL 注入风险
- 激活回调能正常创建默认配置
- 停用回调不删除数据
- 卸载回调清理所有插件数据(配置、数据表、文件)
- config.default.php 包含所有配置项的默认值
- save_settings 使用白名单方式保存字段
- 前端资源路径正确,附加版本号防缓存
- 目录权限正确(config 目录可写)
- PHP 最低版本声明与实际使用语法一致
- README.md 包含安装说明和使用方法