LiteLog 核心程序说明文档 v1.0

2026-10-05 分类:Lite 作者:zhangwei 浏览 43

LiteLog 核心程序说明文档 v1.0

只需阅读这一个文件,即可对 LiteLog 内核进行二次开发。
本文档覆盖目录结构、启动流程、核心类、数据模型、控制器、模板系统、插件机制、数据库结构、二次开发常见场景。


目录

  1. 项目结构总览
  2. 启动流程与请求生命周期
  3. 全局常量与变量
  4. 核心类速查
  5. 数据模型速查
  6. 前台控制器
  7. 后台模块
  8. 路由机制
  9. 模板(主题)系统
  10. 插件与钩子系统
  11. 数据库结构
  12. 二次开发常见场景
  13. 开发规范
  14. 调试与排查

一、项目结构总览

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_ROOTindex.php/www/site/项目根目录,末尾带斜杠
INCLUDE_PATHinit.php/www/site/include/内核目录
CONTENT_PATHinit.php/www/site/content/用户内容目录
ADMIN_PATHinit.php/www/site/admin/后台目录
DB_PREFIXinit.phplt_数据表前缀
BLOG_URLinit.phphttps://ex.com/站点根 URL,末尾带斜杠
TEMPLATE_URLinit.phphttps://ex.com/content/templates/nebula/当前主题 URL
LITELOG_VERSIONinit.php1.0.0版本号

3.2 全局变量

变量类型说明
$DBDatabase数据库实例
$CACHECache缓存实例
$_SESSION['uid']int当前登录用户 ID(登录后才有)
$_SESSION['role']string当前用户角色
$_GET / $_POSTarray由 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/别名.html
  • url_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()执行一次完整请求分发

执行流程:

  1. 触发 dispatch_start
  2. 解析 $_SERVER['REQUEST_URI'] → 填充 $_GET
  3. 匹配路由 → 确定 controller 和 method
  4. 触发 dispatch_matched(可改 $matched)
  5. 实例化并调用
  6. 触发 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() 方法。

控制器方法触发场景使用模板
LogControllerdisplay()首页log_list.php
LogControllerdisplayContent()文章详情echo_log.php
SortControllerdisplay()分类页log_list.php
TagControllerdisplay()标签页log_list.php
AuthorControllerdisplay()作者页log_list.php
RecordControllerdisplay()归档页log_list.php
CommentControllerpost()提交评论无(重定向)

控制器典型结构

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

后台视图约定

每个视图:

  1. 头部 if (!defined('LITELOG_ROOT')) exit('error!');
  2. 用 ob_start() 缓冲内容
  3. 结尾 $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:

字段类型说明
gidINT文章 ID
titleVARCHAR(255)标题
dateBIGINT时间戳
contentLONGTEXT渲染后的 HTML(前台直接输出)
content_mdLONGTEXTMarkdown 源码(后台编辑用)
excerptTEXT摘要(可为空,前台自动截取)
aliasVARCHAR(255)URL 别名
authorINT作者 UID
sortidINT分类 ID(0 = 未分类)
typeVARCHAR(20)blog / page
viewsINT浏览数
comnumINT评论数
topENUMy / n 置顶
hideENUMy / n 隐藏
allow_remarkENUMy / n 允许评论

lt_option(重要):

  • option_name UNIQUE
  • option_value LONGTEXT
  • autoload y 的配置在 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 命名规范

类型规则示例
类名CamelCaseLogModel、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 排查顺序

  1. 看 PHP 错误日志:/www/wwwlogs/xxx.error.log 或临时开 display_errors
  2. 看 SQL 错误日志:content/backup/sql-error.log
  3. 检查 PHP 版本:宝塔 → 网站 → PHP 版本 ≥ 7.4
  4. 检查列/表是否存在:phpMyAdmin 里查
  5. 检查文件权限: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 插件不生效

  1. content/plugins/插件名/插件名.php 目录结构是否正确
  2. active_plugins 配置是否包含插件名(用 SELECT * FROM lt_option WHERE option_name='active_plugins' 查)
  3. 钩子名拼写是否与内核一致
  4. 打开 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.phpPDO 封装,所有 SQL 都经过它
include/lib/option.php配置读写(内存缓存)
include/lib/cache.php文件缓存(content/cache/*.php)
include/lib/hook.php钩子注册与触发
include/lib/url.phpURL 生成(受 url_rewrite 影响)
include/lib/view.php模板定位(带 default 回退)和渲染
include/lib/dispatcher.php解析 URL → 匹配路由 → 调用控制器
include/lib/function.base.phph()、redirect()、makeAlias() 等
include/lib/function.common.phploadActivePlugins()、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 的内核设计哲学:

  1. 轻:核心代码少、依赖少、加载快
  2. 简单:原生 PHP 模板,无需学习模板语法
  3. 可扩展:钩子机制 + 插件/主题系统
  4. 安全:PDO 预处理、h() 转义、目录名校验
  5. 兼容: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 稳定(已公开的方法不要改签名)
  • 新增功能用新钩子,不要覆写旧逻辑

这就是 LiteLog 内核的全部。 有疑问优先查本文档,其次查《插件开发规范》和《主题开发规范》。

评论 (0)

还没有评论,来说点什么吧~

发表评论

正在回复
已有账号?登录后评论