LiteLog 插件开发规范 v1.0
只需阅读这一个文件,就能开发出一个完整可用的 LiteLog 插件。
本文档覆盖目录结构、钩子机制、可用 API、生命周期、开发示例、常见问题。
目录
一、快速开始
最小可运行插件(5 行代码)
在 content/plugins/hello/hello.php 创建文件:
<?php
/**
* Plugin Name: Hello World
* Description: 我的第一个插件
* Version: 1.0.0
* Author: 你的名字
*/
if (!defined('LITELOG_ROOT')) exit('error!');
Hook::addAction('template_footer', function() {
echo '<div style="text-align:center;padding:10px;color:#999">Hello from my plugin!</div>';
});
启用:后台 → 插件 → 找到"Hello World" → 点击"启用"。
效果:打开前台任意页面,页脚出现一行文字。
二、目录结构
强制约定
插件的目录名和主文件名必须完全一致,否则无法被系统识别。
content/plugins/
└── my_plugin/ ← 目录名
├── my_plugin.php ← 主文件(名字必须与目录名一致)
├── my_plugin_callback.php ← 生命周期回调(可选)
├── my_plugin_setting.php ← 后台设置页(可选)
├── my_plugin_show.php ← 前台页面(可选)
├── assets/ ← 静态资源(可选)
│ ├── style.css
│ └── script.js
└── preview.jpg ← 插件图标(可选)
命名规则
| 项 | 规则 | 示例 |
|---|---|---|
| 目录名 | 小写字母、数字、下划线、短横线 | site_stats、read-progress |
| 主文件名 | 与目录名一致 | site_stats.php |
| 函数名 | 建议用 前缀_函数名 避免冲突 | SiteStats_render() |
| 类名 | 建议用 CamelCase | SiteStats_Helper |
禁止事项
- ❌ 不要用中文、空格、特殊符号命名
- ❌ 不要把插件文件直接放在
content/plugins/根目录 - ❌ 不要修改 LiteLog 内核文件(
include/、admin/)
三、头部注释规范
主文件(插件名.php)最顶部必须包含注释头:
<?php
/**
* Plugin Name: 插件显示名称
* Description: 一句话描述插件功能
* Version: 1.0.0
* Author: 作者名
* Author URI: https://example.com
*/
字段说明
| 字段 | 必填 | 说明 |
|---|---|---|
Plugin Name | ✅ | 后台显示的名称(也接受 Name) |
Description | 否 | 一句话描述 |
Version | 否 | 版本号,建议语义化 |
Author | 否 | 作者名 |
Author URI | 否 | 作者主页(也接受 Author URL) |
格式要求
- 冒号必须是英文
: - 注释块必须在文件最前面(前面不能有任何输出)
- 用
/** ... /或/ ... */ - 描述中不要含
*/
四、生命周期回调
在 插件名_callback.php 里定义以下函数,系统会在对应时机自动调用:
<?php
if (!defined('LITELOG_ROOT')) exit('error!');
/**
* 插件启用时调用(后台点击"启用")
* 适合:初始化默认配置、建表、写缓存
*/
function callback_init() {
// 示例:写入默认配置
if (Option::get('my_plugin_installed', '') === '') {
Option::set('my_plugin_installed', time());
Option::set('my_plugin_count', 0);
}
}
/**
* 插件更新时调用(后台点击"更新",目前仅在切换插件版本时触发)
* 适合:升级数据结构
*/
function callback_up() {
// 版本迁移逻辑
}
/**
* 插件删除时调用(后台删除插件目录后)
* 适合:清理数据
*/
function callback_rm() {
// 谨慎使用:这里可以清理你写入 lt_option 的配置
// 但不要在 init 里主动删,避免用户重新启用后丢数据
}
注意:callback_init() 里不要直接操作数据库结构(避免频繁建表),推荐只做配置初始化。
五、钩子机制
5.1 基本用法
LiteLog 使用经典的 Hook 机制,只有两个 API:
// 注册钩子:把我的函数挂到某个钩子上
Hook::addAction($hookName, $callback, $priority = 10);
// 触发钩子:由内核在特定位置调用
Hook::doAction($hookName, ...$args);
5.2 注册示例
// 匿名函数
Hook::addAction('template_footer', function() {
echo '<p>页脚附加内容</p>';
});
// 普通函数
Hook::addAction('log_content', 'MyPlugin_renderLogFooter');
function MyPlugin_renderLogFooter($log) {
echo '<div>文章 ID:' . (int)$log['gid'] . '</div>';
}
// 带优先级(数字小的先执行)
Hook::addAction('template_head', 'MyPlugin_css', 5); // 先执行
Hook::addAction('template_head', 'MyPlugin_js', 20); // 后执行
5.3 修改数据(引用传参)
很多钩子参数是通过引用传递的,你可以在插件里直接修改:
Hook::addAction('log_list_query', function(&$cond, $page, $perPage) {
// 首页只显示指定分类的文章
if (empty($cond['sortid'])) {
$cond['sortid'] = 2;
}
});
5.4 移除钩子
Hook::removeAction('template_footer', 'MyPlugin_renderLogFooter'); // 移除指定函数
Hook::removeAction('template_footer'); // 移除该钩子所有
5.5 钩子清单(共 47 个)
见文末附录。
六、可用 API
6.1 常量
| 常量 | 说明 |
|---|---|
BLOG_URL | 站点根 URL,带末尾斜杠,如 https://example.com/ |
TEMPLATE_URL | 当前主题的 URL,如 https://example.com/content/templates/default/ |
CONTENT_PATH | content/ 目录的绝对路径(末尾带 /) |
INCLUDE_PATH | include/ 目录绝对路径 |
ADMIN_PATH | admin/ 目录绝对路径 |
LITELOG_ROOT | 项目根目录绝对路径 |
DB_PREFIX | 数据表前缀,默认 lt_ |
6.2 配置:Option
// 读取(带默认值)
$val = Option::get('site_title', 'Default');
// 写入(存在则更新,不存在则创建)
Option::set('my_plugin_count', 42);
// 读取所有 autoload=y 的配置
$all = Option::all();
约定:插件配置项命名建议加插件前缀,如 my_plugin_xxx,避免与系统配置冲突。
6.3 数据库:Database
global $DB;
// 查询(预处理防注入)
$rows = $DB->fetchAll("SELECT * FROM " . DB_PREFIX . "blog WHERE hide='n' LIMIT 10");
$row = $DB->fetchOne("SELECT * FROM " . DB_PREFIX . "blog WHERE gid=?", [1]);
$cnt = $DB->fetchColumn("SELECT COUNT(*) FROM " . DB_PREFIX . "comment");
// 插入
$id = $DB->insert('blog', ['title' => '标题', 'date' => time()]);
// 更新
$DB->update('blog', ['title' => '新标题'], 'gid=?', [1]);
// 删除
$DB->delete('blog', 'gid=?', [1]);
// 原始查询(返回 PDOStatement)
$stmt = $DB->query("SELECT * FROM " . DB_PREFIX . "user WHERE role=?", ['admin']);
注意:表名不要写 lt_blog,永远用 DB_PREFIX 拼接。
6.4 内置数据模型
LogModel(文章)
LogModel::query($page, $perPage, $cond, &$total); // 分页查询
LogModel::count(); // 总文章数
LogModel::getOne($gid); // 按 ID 查
LogModel::getByAlias($alias); // 按别名查
LogModel::addView($gid); // 浏览数 +1
LogModel::add($data); // 新增,返回 gid
LogModel::update($gid, $data); // 更新
LogModel::delete($gid); // 删除
LogModel::latest($limit = 10); // 最新 N 篇
LogModel::prev($gid); // 上一篇
LogModel::next($gid); // 下一篇
SortModel(分类)
SortModel::all();
SortModel::getOne($sid);
SortModel::getByAlias($alias);
SortModel::countInSort($sid);
SortModel::add($data);
SortModel::update($sid, $data);
SortModel::delete($sid);
TagModel(标签)
TagModel::all();
TagModel::getByName($name);
TagModel::getGidsByTag($tid);
TagModel::getByGid($gid);
TagModel::attach($gid, 'tag1,tag2'); // 绑定标签
TagModel::detach($gid); // 解绑全部
CommentModel(评论)
CommentModel::byLog($gid);
CommentModel::countByLog($gid);
CommentModel::add($data);
CommentModel::latest($limit = 10);
CommentModel::delete($cid);
UserModel(用户)
UserModel::getOne($uid);
UserModel::getByUsername($username);
UserModel::login($username, $password); // 返回 user 数组或 false
UserModel::update($uid, $data);
LinkModel(链接)
LinkModel::all();
6.5 URL 生成
Url::log($gid, $alias); // https://example.com/post/123.html
Url::sort($sid, $alias); // https://example.com/sort/tech
Url::tag($name); // https://example.com/tag/php
Url::author($uid); // https://example.com/author/1
Url::record('2026-10'); // https://example.com/record/2026-10
6.6 通用函数
h($str); // HTML 转义输出
redirect($url); // 302 跳转并退出
makeAlias($str); // 生成 URL 友好字符串
dateFmt($timestamp); // 时间戳 → 'Y-m-d'
datetimeFmt($timestamp); // 时间戳 → 'Y-m-d H:i'
stripTags($html, $len); // 去标签 + 截断
getExcerpt($html, $len); // 提取摘要
isLoggedIn(); // 是否已登录
checkLogin(); // 未登录则跳转到登录页
getPageNumber(); // 获取当前页码(?page=)
pagination($page, $total, $perPage); // 生成分页 HTML
6.7 数据表结构(带 DB_PREFIX 前缀)
| 表 | 主要字段 |
|---|---|
option | option_name, option_value, autoload |
user | uid, username, password, nickname, email, role |
sort | sid, sortname, alias, pid, description |
blog | gid, title, date, content, content_md, excerpt, alias, author, sortid, type, views, comnum, top, hide, allow_remark |
comment | cid, gid, pid, date, poster, comment, email, url, ip, hide |
tag | tid, tagname |
tag_blog | gid, tid |
link | id, sitename, siteurl, description |
attachment | aid, gid, filename, filesize, date |
七、插件设置页
在后台插件列表中,如果插件目录下有 插件名_setting.php,会自动出现"设置"入口。
约定
- 文件路径:
content/plugins/插件名/插件名_setting.php - 文件顶部必须
require init.php并检查登录 - 页面用后台的
layout.php作为外壳 - 提交后自己处理保存逻辑
完整示例:my_plugin_setting.php
<?php
define('LITELOG_ROOT', dirname(__DIR__, 3) . '/');
require_once LITELOG_ROOT . 'init.php';
require_once ADMIN_PATH . 'helpers.php';
checkLogin();
// 处理表单提交
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
Option::set('my_plugin_color', trim($_POST['color'] ?? '#2563eb'));
Option::set('my_plugin_text', trim($_POST['text'] ?? ''));
redirect(BLOG_URL . 'admin/?action=plugins&saved=1');
}
// 读取当前值
$color = Option::get('my_plugin_color', '#2563eb');
$text = Option::get('my_plugin_text', '欢迎访问本站');
ob_start();
?>
<div class="card" style="max-width:640px">
<div class="card-title">Hello 插件设置</div>
<form method="post" class="form">
<label>主题色</label>
<input type="text" name="color" value="<?= h($color) ?>" placeholder="#2563eb">
<label>展示文案</label>
<input type="text" name="text" value="<?= h($text) ?>" placeholder="欢迎访问本站">
<div style="margin-top:20px">
<button class="btn" type="submit">保存设置</button>
</div>
</form>
</div>
<?php
$content = ob_get_clean();
include ADMIN_PATH . 'views/layout.php';
关键点
dirname(DIR, 3)从content/plugins/插件名/往上 3 层到项目根目录- 必须
checkLogin()防止未登录访问 - 用
admin/helpers.php提供的adminIcon()、navLink()保持风格一致 - 保存后
redirect()防止表单重复提交
八、插件前台页面
如果插件需要前台页面(如"留言板"、"统计页"),用 ?action=xxx 触发。
8.1 前端页面钩子
系统在 Dispatcher 里会触发:
Hook::doAction('public_action_' . $act);
其中 $act 是 URL 中 ?action= 的值(只保留 [a-z0-9_])。
8.2 注册示例
// 访问 https://example.com/?action=my_plugin_show 时触发
Hook::addAction('public_action_my_plugin_show', function() {
// 输出你的页面 HTML
header('Content-Type: text/html; charset=utf-8');
echo '<!DOCTYPE html><html><body>';
echo '<h1>这是插件自定义页面</h1>';
echo '</body></html>';
exit;
});
8.3 更规范的做法:独立渲染文件
my_plugin.php:
Hook::addAction('public_action_my_plugin_show', 'MyPlugin_showPage');
function MyPlugin_showPage() {
include __DIR__ . '/my_plugin_show.php';
exit;
}
my_plugin_show.php:
<?php
if (!defined('LITELOG_ROOT')) exit('error!');
$page_title = '留言板 - ' . Option::get('site_title');
include View::resolve('header'); // 复用主题 header
?>
<div class="layout">
<div class="main-col">
<div class="post-single">
<h1>留言板</h1>
<p>这是插件自定义的前台页面。</p>
</div>
</div>
<aside class="side-col">
<?php include View::resolve('side'); ?>
</aside>
</div>
<?php include View::resolve('footer'); ?>
访问:https://example.com/?action=my_plugin_show
九、数据存储
9.1 使用 Option(推荐)
适合:简单的键值配置(颜色、开关、文本)
Option::set('my_plugin_key', 'value');
$val = Option::get('my_plugin_key', 'default');
优点:不需要建表;缺点:不适合大量数据。
9.2 使用自定义表
适合:需要保存大量记录(如点赞记录、浏览日志)
在 callback_init() 里建表:
function callback_init() {
global $DB;
$sql = "CREATE TABLE IF NOT EXISTS `" . DB_PREFIX . "my_plugin_log` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`gid` INT UNSIGNED NOT NULL,
`ip` VARCHAR(45) DEFAULT '',
`created_at` BIGINT DEFAULT 0,
KEY `idx_gid` (`gid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
$DB->query($sql);
}
优点:结构清晰、查询高效;缺点:需要处理建表和清理。
9.3 存储路径
- 文件:
CONTENT_PATH . 'uploads/插件名/' - 缓存:
CONTENT_PATH . 'cache/插件名_xxx.php'(用$CACHE对象或直接文件读写)
十、完整示例
功能:文章点赞插件。前台显示点赞按钮,点击后计数 +1,同一 IP 24 小时内只能点一次。
目录结构
content/plugins/like/
├── like.php
└── like_callback.php
like.php
<?php
/**
* Plugin Name: 文章点赞
* Description: 为每篇文章添加点赞按钮,防刷
* Version: 1.0.0
* Author: LiteLog
* Author URI: https://example.com
*/
if (!defined('LITELOG_ROOT')) exit('error!');
// 1. 在文章内容后插入点赞按钮
Hook::addAction('log_content_after', 'Like_renderButton');
// 2. 处理 AJAX 请求
Hook::addAction('public_action_like_do', 'Like_handleAjax');
function Like_renderButton($log) {
global $DB;
$gid = (int)$log['gid'];
$count = (int)$DB->fetchColumn(
"SELECT COUNT(*) FROM " . DB_PREFIX . "my_like WHERE gid=?",
[$gid]
);
$liked = Like_hasLiked($gid);
?>
<div class="like-wrap" data-gid="<?= $gid ?>" style="margin:24px 0;text-align:center">
<button class="like-btn<?= $liked ? ' liked' : '' ?>"
data-gid="<?= $gid ?>"
style="padding:10px 28px;border:1px solid <?= $liked ? '#2563eb' : '#e5e9f0' ?>;
background:<?= $liked ? '#2563eb' : '#fff' ?>;
color:<?= $liked ? '#fff' : '#475569' ?>;
border-radius:24px;cursor:pointer;font-size:15px;font-family:inherit;
transition:all .15s">
❤ 点赞 <span class="like-count"><?= $count ?></span>
</button>
<p style="color:#94a3b8;font-size:12.5px;margin:8px 0 0">每 24 小时可点赞一次</p>
</div>
<script>
(function(){
var btn = document.querySelector('.like-btn[data-gid="<?= $gid ?>"]');
if (!btn) return;
btn.addEventListener('click', function () {
if (btn.disabled) return;
btn.disabled = true;
fetch('<?= BLOG_URL ?>?action=like_do&gid=<?= $gid ?>', { credentials: 'same-origin' })
.then(function(r){ return r.json(); })
.then(function(res){
if (res.ok) {
btn.classList.add('liked');
btn.style.background = '#2563eb';
btn.style.color = '#fff';
btn.style.borderColor = '#2563eb';
btn.querySelector('.like-count').textContent = res.count;
} else {
alert(res.msg || '点赞失败');
}
})
.catch(function(){ alert('网络错误'); })
.finally(function(){ btn.disabled = false; });
});
})();
</script>
<?php
}
function Like_handleAjax() {
header('Content-Type: application/json; charset=utf-8');
$gid = (int)($_GET['gid'] ?? 0);
if (!$gid) {
echo json_encode(['ok' => false, 'msg' => '参数错误'], JSON_UNESCAPED_UNICODE);
exit;
}
global $DB;
if (Like_hasLiked($gid)) {
$count = (int)$DB->fetchColumn("SELECT COUNT(*) FROM " . DB_PREFIX . "my_like WHERE gid=?", [$gid]);
echo json_encode(['ok' => false, 'msg' => '24 小时内已经点过啦', 'count' => $count], JSON_UNESCAPED_UNICODE);
exit;
}
$DB->insert('my_like', [
'gid' => $gid,
'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
'created_at' => time(),
]);
$count = (int)$DB->fetchColumn("SELECT COUNT(*) FROM " . DB_PREFIX . "my_like WHERE gid=?", [$gid]);
// 记录到 session 防刷
$_SESSION['like_' . $gid] = time();
echo json_encode(['ok' => true, 'count' => $count], JSON_UNESCAPED_UNICODE);
exit;
}
/**
* 判断当前用户对该文章是否已点赞
*/
function Like_hasLiked($gid) {
global $DB;
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
$row = $DB->fetchOne(
"SELECT created_at FROM " . DB_PREFIX . "my_like WHERE gid=? AND ip=? ORDER BY id DESC LIMIT 1",
[$gid, $ip]
);
if (!$row) return false;
return time() - (int)$row['created_at'] < 86400;
}
like_callback.php
<?php
if (!defined('LITELOG_ROOT')) exit('error!');
function callback_init() {
global $DB;
$sql = "CREATE TABLE IF NOT EXISTS `" . DB_PREFIX . "my_like` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`gid` INT UNSIGNED NOT NULL,
`ip` VARCHAR(45) DEFAULT '',
`created_at` BIGINT DEFAULT 0,
KEY `idx_gid` (`gid`),
KEY `idx_ip` (`ip`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
$DB->query($sql);
}
function callback_rm() {
// 用户删除插件时不自动删表(防止误删数据)
// 若需要,可在此处提示用户在数据库中手动清理:lt_my_like
}
使用
- 上传
content/plugins/like/目录 - 后台 → 插件 → 启用"文章点赞"
- 打开前台任意文章,看到点赞按钮
- 点击 → 数字 +1
十一、开发规范
11.1 命名规范
| 类型 | 规则 | 示例 |
|---|---|---|
| 钩子名 | 小写 + 下划线 | template_footer |
| 函数名 | 插件名首字母大写_功能 | Like_renderButton |
| 类名 | CamelCase | LikeHelper |
| 常量 | PLUGIN_NAME_XXX | LIKE_CACHE_TTL |
| Option 键 | 插件名_xxx | like_color |
| 数据表 | 带前缀,建议 my_xxx | lt_my_like |
11.2 安全规范
- ✅ 所有输出用
h()转义:echo h($userInput) - ✅ 所有 SQL 用参数化:
$DB->fetchOne("...", [$param]) - ✅ 检查登录:敏感操作前调用
checkLogin() - ✅ POST 提交加校验:
$_SERVER['REQUEST_METHOD'] === 'POST' - ❌ 不要直接
echo $_GET['x'] - ❌ 不要把用户输入拼进 SQL
- ❌ 不要在插件里
eval()、exec()、system()
11.3 性能规范
- 避免在每个请求里都
SELECT *查大表,用COUNT或加LIMIT - 频繁读取的配置用
Option::get()(有内存缓存) - 频繁查询的结果用
$CACHE或$_SESSION缓存 - 图片/JS/CSS 放
assets/目录,用BLOG_URL引用
11.4 兼容规范
- 不要修改内核文件(
include/、admin/) - 不要覆盖主题文件(除非是主题插件)
- 不要用
exit()之外的输出终止:钩子里echo就直接输出;public_action_*里要exit - 不要在钩子里抛异常:用
try/catch包起来,避免整个页面 500
十二、常见问题
Q1:插件启用后没反应
- 检查目录结构:
content/plugins/你的插件名/你的插件名.php - 检查后台插件列表是否显示"已启用"
- 临时打开
init.php里的display_errors看有没有报错 - 检查钩子名拼写是否与附录一致
Q2:钩子函数没被调用
- 钩子名是否完全一致(大小写敏感)
- 参数列表是否匹配(引用参数用
&$var) - 插件是否真的启用了(后台查看)
Q3:想在钩子里修改数据,但没生效
钩子参数中标注 &$var 的是引用传参,可以修改;其他是值传递。检查你注册的回调里参数是否带了 &。
Q4:插件和主题都想用钩子
可以。主题文件里同样可以 Hook::addAction(),也能主动调用 Hook::doAction('自定义钩子')。若你的主题想提供专属钩子,用 主题名_xxx 命名,避免与内核冲突。
Q5:如何调试插件?
临时打开调试:编辑 init.php,把
error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_WARNING);
ini_set('display_errors', 0);
改为
error_reporting(E_ALL);
ini_set('display_errors', 1);
然后在插件里 var_dump($data); exit; 查看数据。
Q6:插件和插件之间怎么交互?
两个插件可以互相触发自定义钩子:
// 插件 A:触发
Hook::doAction('my_shared_hook', $data);
// 插件 B:监听
Hook::addAction('my_shared_hook', 'PluginB_handler');
约定:自定义钩子名建议 插件名_xxx 前缀,避免与内核冲突。
附录:钩子速查表
请求生命周期
| 钩子 | 参数 | 说明 |
|---|---|---|
dispatch_start | — | 请求最开始 |
dispatch_matched | &$matched | 路由匹配后,可改写 controller/method |
dispatch_end | — | 请求结束 |
public_action_{name} | — | ?action=name 触发 |
首页 / 文章列表
| 钩子 | 参数 | 说明 |
|---|---|---|
index_head | — | 首页头部 |
log_list_query | &$cond, $page, $perPage | 查询条件(可改) |
log_list_result | &$logs, $page, $total | 查询结果(可改) |
list_page_before | $pageType | 列表页顶部 |
list_item_before | $log, $pageType | 每个文章项之前 |
list_item_after | $log, $pageType | 每个文章项之后 |
list_page_after | $pageType | 列表页底部 |
文章详情
| 钩子 | 参数 | 说明 |
|---|---|---|
log_display_before | &$log | 详情页数据加载后、渲染前 |
log_display_after | &$log | 详情页渲染前的最后一步 |
log_view_before | $log | 正文区前(HTML 中) |
log_view_after | $log | 正文区后 |
log_content | $log | 内容区(通用) |
log_content_before | $log | 正文 HTML 之前 |
log_content_after | $log | 正文 HTML 之后 |
分类 / 标签 / 作者 / 归档
| 钩子 | 参数 | 说明 |
|---|---|---|
sort_display_query | $sort, &$cond, $page, $perPage | 分类页查询前 |
sort_display | $sort, &$logs | 分类页渲染前 |
tag_display_query | $tag, &$cond, $page, $perPage | 标签页查询前 |
tag_display | $tag, &$logs | 标签页渲染前 |
author_display_query | $author, &$cond, $page, $perPage | 作者页查询前 |
author_display | $author, &$logs | 作者页渲染前 |
record_display_query | $year, $month, &$cond, $page, $perPage | 归档页查询前 |
record_display | $year, $month, &$logs | 归档页渲染前 |
评论
| 钩子 | 参数 | 说明 |
|---|---|---|
comment_save_before | &$data | 提交前(可拦截) |
comment_save_after | $cid, $data | 入库后 |
comment_display_before | $comment | 前台每条评论前 |
comment_display_after | $comment | 前台每条评论后 |
comment_form_before | $log | 评论表单之前 |
comment_form_fields | $log | 表单内字段区 |
comment_form_after | $log | 评论表单之后 |
模板(前台)
| 钩子 | 参数 | 说明 |
|---|---|---|
template_head | — | <head> 内,CSS 后 |
header_meta | — | meta 标签之后 |
header_after | — | </header> 之后 |
before_content | — | 主内容区之前 |
after_content | — | 主内容区之后 |
sidebar_before | — | 侧栏之前 |
sidebar_after | — | 侧栏之后 |
footer_before | — | 页脚之前 |
template_footer | — | 页脚内部(脚本) |
后台
| 钩子 | 参数 | 说明 |
|---|---|---|
admin_request | $action | 后台请求开始 |
admin_head | — | 后台 <head> |
admin_menu | $activeKey | 侧栏菜单(可追加) |
admin_footer | — | 后台 </body> 前 |
admin_dashboard_widgets | — | 概览页小工具区 |
log_save_before | &$data, $gid | 文章保存前(前台也触发) |
log_save_after | $gid, $data | 文章保存后 |
admin_log_save_before | &$data, $gid | 后台保存文章前 |
admin_log_save_after | $gid, $data | 后台保存文章后 |
log_delete_before | $gid | 文章删除前 |
log_delete_after | $gid | 文章删除后 |
用户 / 扩展管理
| 钩子 | 参数 | 说明 |
|---|---|---|
user_login | $user | 登录成功 |
user_logout | $uid | 登出前 |
plugin_activate | $name | 插件启用后 |
plugin_deactivate | $name | 插件停用后 |
theme_activate | $name, $old | 主题切换后 |
option_save | $key, $value | 系统配置保存时 |
结语
开发一个 LiteLog 插件的步骤:
- 在
content/plugins/下建目录,名字用英文小写 - 创建同名主文件,顶部写
Plugin Name等注释 - 用
Hook::addAction('钩子名', '函数名')挂载功能 - 需要初始化数据 → 加
插件名_callback.php的callback_init() - 需要配置界面 → 加
插件名_setting.php - 需要前台页面 → 挂
public_action_xxx,或加插件名_show.php - 后台 → 插件 → 启用
调试三件套:
- 临时打开
display_errors var_dump()/error_log()- 浏览器 F12 看 Console / Network
记住三不要:
- ❌ 不改内核
- ❌ 不裸拼 SQL
- ❌ 不直接 echo 用户输入
评论 (0)
发表评论