LiteLog 核心程序说明文档 v1.0
LiteLog 核心程序说明文档 v1.0
只需阅读这一个文件,即可对 LiteLog 内核进行二次开发。
本文档覆盖目录结构、启动流程、核心类、数据模型、控制器、模板系统、插件机制、数据库结构、二次开发常见场景。
目录
一、项目结构总览
litelog/
├── index.php ← 前台入口
├── install.php ← 安装向导(安装后应删除)
├── config.php ← 数据库配置(安装时生成)
├── init.php ← 初始化脚本(所有请求都会经过)
├── .htaccess ← Apache 伪静态
│
├── include/ ← 内核
│ ├── lib/ ← 基础类库
│ │ ├── mysql.php ← Database 类(PDO 封装)
│ │ ├── option.php ← Option 类(配置读写)
│ │ ├── cache.php ← Cache 类(文件缓存)
│ │ ├── hook.php ← Hook 类(插件钩子)
│ │ ├── url.php ← Url 类(URL 生成)
│ │ ├── view.php ← View 类(模板定位与渲染)
│ │ ├── dispatcher.php ← Dispatcher 类(路由分发)
│ │ ├── function.base.php ← 基础函数(h、redirect 等)
│ │ └── function.common.php ← 公共函数(插件加载、分页等)
│ ├── model/ ← 数据模型(M)
│ │ ├── log_model.php
│ │ ├── comment_model.php
│ │ ├── user_model.php
│ │ ├── sort_model.php
│ │ ├── tag_model.php
│ │ └── link_model.php
│ └── controller/ ← 前台控制器(C)
│ ├── log_controller.php
│ ├── sort_controller.php
│ ├── tag_controller.php
│ ├── author_controller.php
│ ├── record_controller.php
│ └── comment_controller.php
│
├── admin/ ← 后台
│ ├── index.php ← 后台入口(自带路由)
│ ├── helpers.php ← 后台辅助函数与图标
│ ├── upload.php ← 图片上传接口
│ ├── assets/ ← 后台静态资源
│ │ ├── admin.css
│ │ └── editor/ ← 自研 Markdown 编辑器
│ └── views/ ← 后台视图
│ ├── layout.php
│ ├── login.php
│ ├── dashboard.php
│ ├── logs.php / log_edit.php
│ ├── sorts.php / sort_edit.php
│ ├── plugins.php / themes.php
│ └── options.php
│
└── content/ ← 用户数据区
├── templates/ ← 主题
│ └── default/ ← 默认主题
├── plugins/ ← 插件
│ └── 插件名/插件名.php
├── cache/ ← 数据缓存
├── uploads/ ← 上传文件
│ └── YYYY/MM/
└── backup/ ← 备份与日志
└── sql-error.log
层次关系
入口(index.php / admin/index.php)
↓
init.php(加载配置、内核、模型、控制器、插件)
↓
Dispatcher(前台)或 switch 路由(后台)
↓
Controller(C)
↓
Model(M)
↓
Database(PDO)
↓
View(V)→ 输出 HTML
二、启动流程与请求生命周期
2.1 前台请求(如访问 /post/123.html)
① 浏览器请求 → index.php
② index.php 定义 LITELOG_ROOT → require init.php
③ init.php 执行:
- 定义常量(INCLUDE_PATH、CONTENT_PATH、ADMIN_PATH)
- 检查 config.php 是否存在(不存在跳 install.php)
- 加载 config.php(数据库信息)
- 定义 DB_PREFIX、BLOG_URL、TEMPLATE_URL
- 加载所有 lib/*.php
- 连接数据库 → 全局 $DB
- Option::init() 加载 autoload 配置
- 创建 $CACHE(文件缓存)
- 加载所有 model/*.php 和 controller/*.php
- loadActivePlugins() 加载启用的插件
④ 返回 index.php → new Dispatcher() → dispatch()
⑤ Dispatcher::dispatch() 执行:
- 触发 dispatch_start 钩子
- 解析 URL 得到 $_GET 参数(post / alias / sort_alias / tag / ...)
- 匹配路由到 Controller::method
- 触发 dispatch_matched 钩子
- 实例化控制器 → 调用方法
⑥ 控制器执行:
- 调用 Model 查数据
- 处理业务逻辑(分页、标签、评论树等)
- 调用 View::display('模板名', $data)
⑦ 模板渲染:
- View::resolve('header') 找主题文件
- include 主题文件(PHP 原生模板)
- 触发 template_head / template_footer 等钩子
- 输出 HTML
⑧ Dispatcher 触发 dispatch_end 钩子 → 请求结束
2.2 后台请求(如访问 /admin/?action=logs)
① 浏览器请求 → admin/index.php
② 定义 LITELOG_ROOT → require init.php(同上)
③ require admin/helpers.php
④ 读 $action = $_GET['action']
⑤ 如果是 login → 处理登录表单;如果是 logout → 销毁 session
⑥ checkLogin() 验证登录
⑦ switch ($action) 路由到对应分支
⑧ 视图组装 $content → include admin/views/layout.php 输出
2.3 钩子触发点(一次前台请求的完整钩子顺序)
dispatch_start
↓
dispatch_matched
↓
index_head(首页)或 log_display_before(详情页)
↓
log_list_query / log_list_result(列表)
↓
[控制器] → View::display()
↓
header_meta → template_head → header_after → before_content
↓
list_page_before / list_item_before / list_item_after / list_page_after(列表页)
或 log_view_before / log_content_before / log_content_after / log_view_after(详情页)
↓
comment_display_before / comment_display_after(每条评论)
↓
after_content → footer_before → template_footer
↓
dispatch_end
三、全局常量与变量
3.1 常量
| 常量 | 定义位置 | 值示例 | 说明 |
|---|---|---|---|
LITELOG_ROOT | index.php | /www/site/ | 项目根目录,末尾带斜杠 |
INCLUDE_PATH | init.php | /www/site/include/ | 内核目录 |
CONTENT_PATH | init.php | /www/site/content/ | 用户内容目录 |
ADMIN_PATH | init.php | /www/site/admin/ | 后台目录 |
DB_PREFIX | init.php | lt_ | 数据表前缀 |
BLOG_URL | init.php | https://ex.com/ | 站点根 URL,末尾带斜杠 |
TEMPLATE_URL | init.php | https://ex.com/content/templates/nebula/ | 当前主题 URL |
LITELOG_VERSION | init.php | 1.0.0 | 版本号 |
3.2 全局变量
| 变量 | 类型 | 说明 |
|---|---|---|
$DB | Database | 数据库实例 |
$CACHE | Cache | 缓存实例 |
$_SESSION['uid'] | int | 当前登录用户 ID(登录后才有) |
$_SESSION['role'] | string | 当前用户角色 |
$_GET / $_POST | array | 由 Dispatcher 或表单填充 |
四、核心类速查
4.1 Database(数据库)
位置:include/lib/mysql.php
获取:global $DB;
| 方法 | 说明 |
|---|---|
query($sql, $params = []) | 执行 SQL,返回 PDOStatement |
fetchAll($sql, $params = []) | 查询多行,返回数组 |
fetchOne($sql, $params = []) | 查询单行,返回数组或 null |
fetchColumn($sql, $params = []) | 查询单值 |
insert($table, $data) | 插入,返回 lastInsertId |
update($table, $data, $where, $whereParams) | 更新,返回 bool |
delete($table, $where, $params) | 删除 |
prefix() | 返回表前缀 |
pdo() | 返回底层 PDO 对象 |
使用示例:
global $DB;
// 查询
$rows = $DB->fetchAll("SELECT * FROM " . DB_PREFIX . "blog WHERE hide='n' LIMIT 10");
$one = $DB->fetchOne("SELECT * FROM " . DB_PREFIX . "user WHERE uid=?", [1]);
$cnt = $DB->fetchColumn("SELECT COUNT(*) FROM " . DB_PREFIX . "comment");
// 写入
$gid = $DB->insert('blog', ['title' => '标题', 'date' => time(), 'author' => 1]);
$DB->update('blog', ['title' => '新标题'], 'gid=?', [$gid]);
$DB->delete('blog', 'gid=?', [$gid]);
错误日志:所有 SQL 错误会写入 content/backup/sql-error.log。
4.2 Option(配置)
位置:include/lib/option.php
初始化:init.php 中自动执行 Option::init()
| 方法 | 说明 |
|---|---|
Option::get($name, $default = '') | 读取配置 |
Option::set($name, $value) | 写入(不存在则创建) |
Option::all() | 返回所有 autoload=y 的配置 |
特性:
- 首次读取时一次性加载所有 autoload='y' 的配置到内存
Option::set()会同时更新内存和数据库
内置配置键:
site_title, site_description, site_key, index_lognum,
current_template, url_rewrite, comment_need_check,
upload_maxsize, active_plugins
4.3 Cache(文件缓存)
位置:include/lib/cache.php
| 方法 | 说明 |
|---|---|
$CACHE->read($key) | 读取缓存,不存在返回 false |
$CACHE->write($key, $data) | 写入缓存 |
$CACHE->delete($key) | 删除单个 |
$CACHE->flush() | 清空全部 |
缓存文件位置:content/cache/{key}.php,内容是 <?php return [...];。
4.4 Hook(钩子)
位置:include/lib/hook.php
| 方法 | 说明 |
|---|---|
Hook::addAction($hook, $callback, $priority = 10) | 注册钩子 |
Hook::doAction($hook, ...$args) | 触发钩子 |
Hook::removeAction($hook, $callback = null) | 移除钩子 |
优先级:数字小的先执行。同优先级按注册顺序。
4.5 Url(URL 生成)
位置:include/lib/url.php
| 方法 | 说明 |
|---|---|
Url::log($gid, $alias) | 文章 URL |
Url::sort($sid, $alias) | 分类 URL |
Url::tag($name) | 标签 URL |
Url::author($uid) | 作者 URL |
Url::record($date) | 归档 URL |
行为:受 Option::get('url_rewrite') 影响。
url_rewrite = 'y'→/post/别名.htmlurl_rewrite = 'n'→?post=123
4.6 View(模板)
位置:include/lib/view.php
| 方法 | 说明 |
|---|---|
View::resolve($name) | 返回主题文件的绝对路径,带 default 回退 |
View::display($name, $data = []) | 渲染模板 |
回退逻辑:当前主题没这个文件 → 用 default 主题的同名文件。
4.7 Dispatcher(路由)
位置:include/lib/dispatcher.php
| 方法 | 说明 |
|---|---|
dispatch() | 执行一次完整请求分发 |
执行流程:
- 触发
dispatch_start - 解析
$_SERVER['REQUEST_URI']→ 填充$_GET - 匹配路由 → 确定 controller 和 method
- 触发
dispatch_matched(可改$matched) - 实例化并调用
- 触发
dispatch_end
五、数据模型速查
所有模型都在 include/model/ 下,使用静态方法调用。
5.1 LogModel(文章)
| 方法 | 返回 | 说明 |
|---|---|---|
query($page, $perPage, $cond, &$total) | array | 分页查询,$total 引用返回总数 |
count() | int | 公开文章总数 |
getOne($gid) | array|null | 按 ID 查 |
getByAlias($alias) | array|null | 按别名查 |
addView($gid) | void | 浏览数 +1 |
add($data) | int | 新增,返回 gid |
update($gid, $data) | bool | 更新 |
delete($gid) | bool | 删除 |
latest($limit = 10) | array | 最新 N 篇(仅 gid/title/date/views/comnum/alias) |
prev($gid) | array|null | 上一篇(gid/title/alias) |
next($gid) | array|null | 下一篇 |
$cond 支持的键:
[
'sortid' => int, // 按分类
'author' => int, // 按作者
'keyword' => string, // 标题或内容模糊匹配
'tagid' => int, // 按标签
'year' => int, // 按年
'month' => int, // 按月
]
5.2 SortModel(分类)
| 方法 | 说明 |
|---|---|
all() | 所有分类 |
getOne($sid) | 按 ID |
getByAlias($alias) | 按别名 |
countInSort($sid) | 该分类文章数 |
add($data) / update($sid, $data) / delete($sid) | 增改删 |
5.3 TagModel(标签)
| 方法 | 说明 |
|---|---|
all() | 所有标签 |
getByName($name) | 按名查 |
getGidsByTag($tid) | 该标签的文章 ID 数组 |
getByGid($gid) | 该文章的所有标签 |
attach($gid, 'tag1,tag2') | 绑定标签(自动创建) |
detach($gid) | 解绑全部 |
5.4 CommentModel(评论)
| 方法 | 说明 |
|---|---|
byLog($gid) | 该文章全部评论(扁平数组) |
countByLog($gid) | 该文章评论数 |
add($data) | 新增,自动+文章 comnum |
latest($limit = 10) | 最新 N 条 |
delete($cid) | 删除,自动-文章 comnum |
5.5 UserModel(用户)
| 方法 | 说明 |
|---|---|
getOne($uid) | 按 ID |
getByUsername($u) | 按用户名 |
login($username, $password) | 验证,成功返回 user 数组,失败返回 false |
update($uid, $data) | 更新 |
5.6 LinkModel(友链)
| 方法 | 说明 |
|---|---|
all() | 所有友链 |
六、前台控制器
控制器都在 include/controller/ 下,每个控制器有一个 display() 或 displayContent() 方法。
| 控制器 | 方法 | 触发场景 | 使用模板 |
|---|---|---|---|
LogController | display() | 首页 | log_list.php |
LogController | displayContent() | 文章详情 | echo_log.php |
SortController | display() | 分类页 | log_list.php |
TagController | display() | 标签页 | log_list.php |
AuthorController | display() | 作者页 | log_list.php |
RecordController | display() | 归档页 | log_list.php |
CommentController | post() | 提交评论 | 无(重定向) |
控制器典型结构
class XxxController {
public function display() {
// 1. 参数获取
$page = getPageNumber();
$perPage = (int)Option::get('index_lognum', 10);
// 2. 查询前钩子
$cond = [];
Hook::doAction('log_list_query', $cond, $page, $perPage);
// 3. 查数据
$total = 0;
$logs = LogModel::query($page, $perPage, $cond, $total);
// 4. 装饰数据
foreach ($logs as &$log) {
$log['url'] = Url::log($log['gid'], $log['alias']);
$log['date_fmt'] = dateFmt($log['date']);
// ...
}
// 5. 查询后钩子
Hook::doAction('log_list_result', $logs, $page, $total);
// 6. 渲染视图
View::display('log_list', [
'logs' => $logs,
'pageHtml' => pagination($page, $total, $perPage),
'pageType' => 'index',
]);
}
}
404 处理
if (!$log) {
header("HTTP/1.0 404 Not Found");
View::display('404');
return;
}
七、后台模块
后台入口:admin/index.php,用 switch ($action) 路由,不支持伪静态。
?action= | 功能 | 视图 |
|---|---|---|
(空)或 dashboard | 概览 | views/dashboard.php |
logs | 文章列表 | views/logs.php |
log_edit | 写/改文章 | views/log_edit.php |
log_delete | 删除文章 | 无(重定向) |
sorts | 分类列表 | views/sorts.php |
sort_edit | 编辑/新增分类 | views/sort_edit.php |
sort_delete | 删除分类 | 无(重定向) |
plugins | 插件管理 | views/plugins.php |
plugin_toggle | 启用/停用 | 无(重定向) |
themes | 主题管理 | views/themes.php |
theme_activate | 切换主题 | 无(重定向) |
options | 系统设置 | views/options.php |
login / logout | 登录/登出 | views/login.php |
后台视图约定
每个视图:
- 头部
if (!defined('LITELOG_ROOT')) exit('error!'); - 用
ob_start()缓冲内容 - 结尾
$content = ob_get_clean(); include DIR . '/layout.php';
layout.php 输出完整 HTML 外壳(左侧栏 + 主区),$content 是主内容。
后台辅助函数(helpers.php)
| 函数 | 说明 |
|---|---|
adminIcon($name) | 输出 SVG 图标 |
navLink(...) | 侧栏导航链接 |
isValidName($name) | 校验目录名是否合法 |
parseHeader($file) | 解析插件/主题头部注释 |
scanPlugins() | 扫描所有插件 |
scanThemes() | 扫描所有主题 |
getActivePlugins() | 获取启用的插件名数组 |
saveActivePlugins($names) | 保存启用的插件名数组 |
八、路由机制
8.1 前台路由(伪静态 + 参数)
Dispatcher 支持两种 URL 形式:
形式 1:查询字符串(默认,无需服务器配置)
https://ex.com/?post=123
https://ex.com/?sort=1
https://ex.com/?tag=php
https://ex.com/?author=1
https://ex.com/?record=2026-10
https://ex.com/?page=2
https://ex.com/?action=xxx
形式 2:伪静态(需开启 url_rewrite + 服务器规则)
https://ex.com/post/123.html
https://ex.com/post/别名.html
https://ex.com/sort/tech
https://ex.com/tag/php
https://ex.com/author/1
https://ex.com/record/2026-10
8.2 路由匹配逻辑
Dispatcher 用 preg_match 逐条尝试:
if (preg_match('|^post/(\d+)\.html$|', $uri, $m)) $_GET['post'] = $m[1];
elseif (preg_match('|^post/([^/]+)\.html$|', $uri, $m)) $_GET['alias'] = $m[1];
elseif (preg_match('|^sort/([^/]+)$|', $uri, $m)) $_GET['sort_alias'] = $m[1];
// ...
然后根据 $_GET 的键决定控制器:
if (isset($_GET['post']) || $_GET['alias']) → LogController::displayContent
if (isset($_GET['sort']) || $_GET['sort_alias']) → SortController::display
// ...
8.3 添加新路由
不要直接修改 Dispatcher 的路由表。推荐:用 dispatch_matched 钩子拦截:
// 在插件里
Hook::addAction('dispatch_matched', function (&$matched) {
if (isset($_GET['my_page'])) {
$matched['controller'] = 'MyPluginController';
$matched['method'] = 'display';
$matched['args'] = [];
}
});
或者用 ?action=xxx 接口:
Hook::addAction('public_action_my_thing', function () {
echo 'Hello';
exit;
});
访问 https://ex.com/?action=my_thing 即可。
8.4 自定义前台接口(API)
推荐用 public_action_xxx:
// 插件
Hook::addAction('public_action_api_ping', function () {
header('Content-Type: application/json');
echo json_encode(['ok' => true, 'time' => time()]);
exit;
});
访问 https://ex.com/?action=api_ping。
九、模板(主题)系统
9.1 主题位置与结构
content/templates/主题名/
├── header.php ← 必需
├── footer.php ← 必需
├── log_list.php ← 必需
├── echo_log.php ← 必需
├── page.php ← 可选
├── 404.php ← 可选
├── side.php ← 可选
└── css/ js/ images/ ← 静态资源
9.2 视图定位
// 解析实际文件路径
$file = View::resolve('header');
// → content/templates/当前主题/header.php(存在则用)
// → content/templates/default/header.php(回退)
// 渲染
View::display('log_list', ['logs' => $logs, ...]);
9.3 模板可用的上下文
所有模板通用:
| 变量 / 函数 | 说明 |
|---|---|
$page_title | 页面标题(模板可自行赋值) |
Option::get() | 配置读取 |
h(), dateFmt() 等 | 基础函数 |
Url::xxx() | URL 生成 |
XxxModel::xxx() | 数据模型 |
Hook::doAction() | 触发钩子 |
列表页额外:
| 变量 | 说明 |
|------|------|
| $logs | 文章数组 |
| $pageHtml | 分页 HTML |
| $pageType | index / sort / tag / author / record |
| $sort / $tag / $author / $recordYear / $recordMonth | 根据页类型 |
详情页额外:
| 变量 | 说明 |
|------|------|
| $log | 文章数组(含 url、date_fmt、tags、sortname、sorturl、author_name) |
| $prev / $next | 上下篇 |
| $comments | 评论树(含 children) |
9.4 后台模板设置
主题可以在 content/templates/主题名/options.php 提供设置界面。目前内核尚未内置主题设置页路由,可通过插件或后续扩展实现。
十、插件与钩子系统
10.1 插件位置与结构
content/plugins/插件名/插件名.php ← 主文件(必需)
content/plugins/插件名/插件名_callback.php ← 生命周期(可选)
content/plugins/插件名/插件名_setting.php ← 后台设置页(可选)
content/plugins/插件名/插件名_show.php ← 前台页面(可选)
强制约定:目录名和主文件名必须一致。
10.2 加载流程
// init.php 中:
loadActivePlugins();
// function.common.php:
function loadActivePlugins() {
$active = Option::get('active_plugins', '');
if (!$active) return;
foreach (explode(',', $active) as $p) {
$file = CONTENT_PATH . 'plugins/' . $p . '/' . $p . '.php';
if (file_exists($file)) require_once $file;
}
}
10.3 钩子机制
| 方法 | 说明 |
|---|---|
Hook::addAction($hook, $callback, $priority = 10) | 注册 |
Hook::doAction($hook, ...$args) | 触发 |
Hook::removeAction($hook, $callback = null) | 移除 |
引用传参:doAction('hook', $ref1, $ref2) 时,回调函数声明为 function ($a, &$b) 即可修改。
10.4 可用钩子清单
见《LiteLog 插件开发规范》附录。共 47 个:
请求生命周期:dispatch_start、dispatch_matched、dispatch_end、public_action_{name}
首页/列表:index_head、log_list_query、log_list_result、list_page_before、list_item_before、list_item_after、list_page_after
文章详情:log_display_before、log_display_after、log_view_before、log_view_after、log_content、log_content_before、log_content_after
分类/标签/作者/归档:sort_display_query、sort_display、tag_display_query、tag_display、author_display_query、author_display、record_display_query、record_display
评论:comment_save_before、comment_save_after、comment_post、comment_display_before、comment_display_after、comment_form_before、comment_form_fields、comment_form_after
模板:header_meta、template_head、header_after、before_content、after_content、footer_before、template_footer、sidebar_before、sidebar_after
后台:admin_head、admin_menu、admin_footer、admin_request、admin_dashboard_widgets、admin_log_save_before、admin_log_save_after
数据:log_save_before、log_save_after、log_delete_before、log_delete_after、plugin_activate、plugin_deactivate、theme_activate、option_save
用户:user_login、user_logout
十一、数据库结构
11.1 表清单(默认前缀 lt_)
| 表 | 用途 | 主要字段 |
|---|---|---|
lt_option | 系统配置 | option_name、option_value、autoload |
lt_user | 用户 | uid、username、password、nickname、role |
lt_sort | 分类 | sid、sortname、alias、pid、description |
lt_blog | 文章 | gid、title、date、content、content_md、alias、author、sortid、views、comnum、hide、allow_remark |
lt_comment | 评论 | cid、gid、pid、date、poster、comment、email、ip、hide |
lt_tag | 标签 | tid、tagname |
lt_tag_blog | 标签-文章关联 | gid、tid |
lt_link | 友链 | id、sitename、siteurl、description |
lt_attachment | 附件 | aid、gid、filename、filesize、date |
11.2 关键字段说明
lt_blog:
| 字段 | 类型 | 说明 |
|---|---|---|
gid | INT | 文章 ID |
title | VARCHAR(255) | 标题 |
date | BIGINT | 时间戳 |
content | LONGTEXT | 渲染后的 HTML(前台直接输出) |
content_md | LONGTEXT | Markdown 源码(后台编辑用) |
excerpt | TEXT | 摘要(可为空,前台自动截取) |
alias | VARCHAR(255) | URL 别名 |
author | INT | 作者 UID |
sortid | INT | 分类 ID(0 = 未分类) |
type | VARCHAR(20) | blog / page |
views | INT | 浏览数 |
comnum | INT | 评论数 |
top | ENUM | y / n 置顶 |
hide | ENUM | y / n 隐藏 |
allow_remark | ENUM | y / n 允许评论 |
lt_option(重要):
option_nameUNIQUEoption_valueLONGTEXTautoloady的配置在 init.php 时一次性加载到内存- 插件配置建议 autoload='y',除非数据很大
十二、二次开发常见场景
场景 1:新增前台页面
需求:增加一个 /about 页面。
做法:不改内核,用插件的 public_action_about:
// content/plugins/my/about.php
Hook::addAction('public_action_about', 'My_aboutPage');
function My_aboutPage() {
$page_title = '关于 - ' . Option::get('site_title');
View::display('page', [
'title' => '关于我',
'content' => '<p>这是一个博客。</p>',
]);
exit;
}
访问 https://ex.com/?action=about。
场景 2:新增数据模型
需求:新增"留言板"数据表操作。
做法:在插件目录新建模型类:
// content/plugins/guestbook/GuestbookModel.php
class GuestbookModel {
public static function all() {
global $DB;
return $DB->fetchAll("SELECT * FROM " . DB_PREFIX . "guestbook ORDER BY id DESC");
}
public static function add($data) {
global $DB;
return $DB->insert('guestbook', $data);
}
}
在插件主文件中 require_once DIR . '/GuestbookModel.php'; 加载。
场景 3:新增后台菜单
需求:在后台侧栏加一个"留言管理"。
做法:
// 1. 在插件中注入菜单
Hook::addAction('admin_menu', function ($activeKey) {
$active = $activeKey === 'guestbook' ? ' active' : '';
echo '<a class="nav-link' . $active . '" href="' . BLOG_URL . 'admin/?action=guestbook">'
. '<span class="nav-icon"></span>'
. '<span class="nav-label">留言</span></a>';
});
// 2. 后台入口拦截请求
Hook::addAction('admin_request', function ($action) {
if ($action === 'guestbook') {
// 渲染你自己的后台页面
$rows = GuestbookModel::all();
ob_start();
?>
<div class="card">
<div class="card-title">留言列表</div>
<table class="table">
<?php foreach ($rows as $r): ?>
<tr><td><?= h($r['name']) ?></td><td><?= h($r['content']) ?></td></tr>
<?php endforeach; ?>
</table>
</div>
<?php
$content = ob_get_clean();
include ADMIN_PATH . 'views/layout.php';
exit;
}
});
注意:admin_request 钩子在 checkLogin() 之后触发,需要已登录。
场景 4:给文章加自定义字段
需求:文章增加"阅读原文链接"。
做法:
// 1. 建表(一次性)
// ALTER TABLE lt_blog ADD COLUMN source_url VARCHAR(255) DEFAULT '';
// 2. 后台保存时写入
Hook::addAction('admin_log_save_before', function (&$data, $gid) {
$data['source_url'] = trim($_POST['source_url'] ?? '');
});
// 3. 后台编辑页表单追加(渲染时)
Hook::addAction('admin_head', function () {
// 也可以用 JS 注入到表单
});
// 4. 前台显示
Hook::addAction('log_content_after', function ($log) {
if (!empty($log['source_url'])) {
echo '<p>原文:<a href="' . h($log['source_url']) . '">'
. h($log['source_url']) . '</a></p>';
}
});
场景 5:新增后台页面(完全自定义)
如果不想用钩子注入,也可以让插件的 xxx_setting.php 独立处理请求:
// content/plugins/xxx/xxx_setting.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('xxx_color', trim($_POST['color'] ?? ''));
redirect(BLOG_URL . 'admin/?action=plugins');
}
ob_start();
?>
<div class="card">
<form method="post" class="form">
<label>主题色</label>
<input name="color" value="<?= h(Option::get('xxx_color', '#2563eb')) ?>">
<button class="btn" type="submit">保存</button>
</form>
</div>
<?php
$content = ob_get_clean();
include ADMIN_PATH . 'views/layout.php';
访问 https://ex.com/content/plugins/xxx/xxx_setting.php 即可。
场景 6:拦截并修改路由
需求:所有文章详情页都改走自定义控制器。
做法:
Hook::addAction('dispatch_matched', function (&$matched) {
if ($matched['controller'] === 'LogController'
&& $matched['method'] === 'displayContent') {
$matched['controller'] = 'MyLogController';
}
});
需要提前定义 MyLogController 类。
场景 7:新增文章保存时的验证
需求:标题少于 3 个字时拒绝保存。
做法:
Hook::addAction('log_save_before', function (&$data, $gid) {
if (mb_strlen($data['title']) < 3) {
// 无法直接中断,可以改写标题
$data['title'] = '短标题:' . $data['title'];
}
});
注意:目前没有"拒绝保存"机制。若要硬拒绝,需在 admin/index.php 的 log_edit 分支扩展钩子返回值检查(不建议改内核,除非必要)。
场景 8:替换模板文件
需求:所有文章详情页在正文后追加一个捐赠按钮。
做法:在主题的 echo_log.php 里加:
<?php Hook::doAction('log_content_after', $log); ?>
或者用插件:
Hook::addAction('log_content_after', function ($log) {
echo '<div class="donate" style="text-align:center;margin:20px 0">'
. '<a href="#" class="btn">☕ 请我喝咖啡</a></div>';
});
十三、开发规范
13.1 命名规范
| 类型 | 规则 | 示例 |
|---|---|---|
| 类名 | CamelCase | LogModel、TagController |
| 方法名 | 小驼峰 | getOne()、fetchAll() |
| 函数名 | 小驼峰 | getExcerpt()、pageUrl() |
| 常量 | 全大写 + 下划线 | BLOG_URL |
| 数据表 | 前缀 + 小写 + 下划线 | lt_blog、lt_tag_blog |
| 钩子名 | 小写 + 下划线 | log_content_after |
| 文件名 | 小写 + 下划线 | log_model.php |
13.2 安全规范
- 所有 SQL 用预处理:
$DB->fetchOne("... WHERE gid=?", [$gid]) - 所有输出用
h():echo h($userInput) - 所有目录名用
isValidName()校验 - 所有敏感操作前调
checkLogin() - 不用
eval()/exec()/system() - 不要在前台暴露
config.php(已在.htaccess拦截,Nginx 需自行配置)
13.3 代码组织
- 内核文件不改:除非有充分理由,否则用插件注入
- 一个类一个文件:
include/model/xxx_model.php - 模型只做数据:业务逻辑放控制器
- 视图只渲染:不写复杂逻辑
- 插件只依赖公开 API:不直接调用内核私有方法
13.4 兼容性
- PHP 7.1+(
??、[]解构需要 7.1) - MySQL 5.6+ / MariaDB 10.3+(utf8mb4 支持)
- 浏览器:现代浏览器(ES5 +
IntersectionObserver检测降级)
十四、调试与排查
14.1 打开错误显示
编辑 init.php:
// 临时改为
error_reporting(E_ALL);
ini_set('display_errors', 1);
排查完改回:
error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_WARNING);
ini_set('display_errors', 0);
14.2 查看 SQL 错误日志
content/backup/sql-error.log
每条格式:
[2026-10-05 12:34:56] SQLSTATE[42S22]: Column not found: 1054 Unknown column 'xxx'
SQL: INSERT INTO ...
PARAMS: {...}
14.3 快速查看变量
<pre><?php var_dump($log); exit; ?></pre>
14.4 检查当前钩子触发情况
在任何 doAction 前后加 error_log():
error_log('before hook: log_content');
Hook::doAction('log_content', $log);
error_log('after hook: log_content');
日志位置:PHP-FPM 错误日志(宝塔:/www/wwwlogs/php-error.log)。
14.5 常见 500 排查顺序
- 看 PHP 错误日志:
/www/wwwlogs/xxx.error.log或临时开display_errors - 看 SQL 错误日志:
content/backup/sql-error.log - 检查 PHP 版本:宝塔 → 网站 → PHP 版本 ≥ 7.4
- 检查列/表是否存在:phpMyAdmin 里查
- 检查文件权限:
content/目录需可写
14.6 前台页面空白
- 打开
display_errors看报错 - 检查主题文件是否缺
header.php(会导致 View 找不到文件) - 检查主题
header.php是否有if (!defined('LITELOG_ROOT')) exit('error!');
14.7 后台页面 500
- 常见原因:
helpers.php未加载、adminIcon未定义 - 检查
admin/index.php顶部是否有require_once ADMIN_PATH . 'helpers.php'; - 检查
admin/views/layout.php里是否重复定义了adminIcon函数
14.8 插件不生效
content/plugins/插件名/插件名.php目录结构是否正确active_plugins配置是否包含插件名(用SELECT * FROM lt_option WHERE option_name='active_plugins'查)- 钩子名拼写是否与内核一致
- 打开
display_errors看插件加载是否报错
14.9 主题切换不生效
- 强刷
Ctrl + Shift + R - 检查
current_template配置 - 检查
content/templates/主题名/header.php是否存在
附录 A:文件职责速查
| 文件 | 职责 |
|---|---|
index.php | 前台入口(3 行:定义常量 → require init → dispatch) |
init.php | 初始化:常量、配置、类加载、DB、Option、插件 |
config.php | 数据库连接信息(安装时生成,含密码,不要提交到公开仓库) |
include/lib/mysql.php | PDO 封装,所有 SQL 都经过它 |
include/lib/option.php | 配置读写(内存缓存) |
include/lib/cache.php | 文件缓存(content/cache/*.php) |
include/lib/hook.php | 钩子注册与触发 |
include/lib/url.php | URL 生成(受 url_rewrite 影响) |
include/lib/view.php | 模板定位(带 default 回退)和渲染 |
include/lib/dispatcher.php | 解析 URL → 匹配路由 → 调用控制器 |
include/lib/function.base.php | h()、redirect()、makeAlias() 等 |
include/lib/function.common.php | loadActivePlugins()、pagination() 等 |
include/model/*.php | 数据访问层,静态方法 |
include/controller/*.php | 前台业务控制器 |
admin/index.php | 后台入口 + 路由(switch) |
admin/helpers.php | 后台辅助函数、图标、扫描插件/主题 |
admin/views/*.php | 后台视图 |
content/templates/*/ | 主题 |
content/plugins/*/ | 插件 |
附录 B:常用代码片段
B.1 生成一个新的前台页面
// 插件主文件里
Hook::addAction('public_action_my_thing', 'My_thingPage');
function My_thingPage() {
$page_title = '自定义页 - ' . Option::get('site_title');
ob_start();
?>
<div class="container">
<h1>自定义页</h1>
<p>内容</p>
</div>
<?php
$content = ob_get_clean();
// 或者用主题的 page.php
View::display('page', [
'title' => '自定义页',
'content' => $content,
]);
exit;
}
B.2 后台增加一个设置项
// 保存
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
Option::set('my_key', trim($_POST['my_key'] ?? ''));
}
// 读取
$value = Option::get('my_key', 'default');
B.3 批量处理文章
$logs = $DB->fetchAll("SELECT gid, content_md FROM " . DB_PREFIX . "blog WHERE type='blog'");
foreach ($logs as $l) {
$newHtml = renderMarkdown($l['content_md']); // 你自己的处理
$DB->update('blog', ['content' => $newHtml], 'gid=?', [$l['gid']]);
}
B.4 判断当前页类型
$pageType = $_GET['post'] ?? $_GET['sort_alias'] ?? $_GET['tag'] ?? 'index';
结语
LiteLog 的内核设计哲学:
- 轻:核心代码少、依赖少、加载快
- 简单:原生 PHP 模板,无需学习模板语法
- 可扩展:钩子机制 + 插件/主题系统
- 安全:PDO 预处理、
h()转义、目录名校验 - 兼容:MySQL 5.6+ / MariaDB 10.3+ / PHP 7.1+
二次开发三条路径:
| 目标 | 推荐方式 | 是否需要改内核 |
|---|---|---|
| 加前台功能 | 插件 + public_action_xxx | 不需要 |
| 加后台功能 | 插件 + admin_menu + admin_request | 不需要 |
| 改数据字段 | ALTER TABLE + log_save_before 钩子 | 不需要 |
| 换页面风格 | 新主题 | 不需要 |
| 加 API | 插件 + public_action_api_xxx | 不需要 |
| 改路由逻辑 | dispatch_matched 钩子 | 不需要 |
核心原则:能钩子就钩子,能插件就插件,能主题就主题,最后才考虑改内核。
改内核时注意:
- 保持向后兼容(旧插件不能失效)
- 保持 API 稳定(已公开的方法不要改签名)
- 新增功能用新钩子,不要覆写旧逻辑
评论 (0)
发表评论