内核框架开发指南
本指南面向 BlogKit 的深度二次开发者,讲解 core/ 内核的实现原理与源码结构。模板标签在主题中的用法见 05-主题开发指南,插件如何使用内核能力见 06-插件开发指南,系统目录全貌见 03-目录与文件说明。
一、内核总览:请求生命周期
BlogKit 是无框架的原生 PHP 应用,不依赖 Composer,也没有依赖注入容器。一个 HTTP 请求的完整处理链路如下:
浏览器请求
|
v
入口文件(index.php / admin.php / api.php)
| require core/bootstrap.php
v
bootstrap.php 统一引导
| 输出缓冲 -> 常量定义 -> 安装检查 -> 自动加载 -> 核心类加载
| -> Config 初始化 -> 时区 -> 会话 -> 滑动过期
| -> 维护/站点状态检查 -> CSRF Token -> 安全响应头
v
入口文件各自的处理逻辑
|- index.php : 静态资源服务 -> 记住我自动登录 -> Router::dispatch()
|- admin.php : IP 白名单 -> action 路由表 -> 登录/权限校验 -> 控制器
`- api.php : system_init 钩子 -> api_dispatch 钩子(插件接管或 404)
|
v
控制器(app/Controllers/) -> 模型(app/Models/) -> Database(PDO)
|
v
Template 模板引擎渲染 -> 响应输出三个入口的分工:
| 入口 | 面向对象 | 路由机制 | 职责要点 |
|---|---|---|---|
| index.php | 前台访客 | Router 类,配置驱动的路由表 | 另负责 /themes/*/assets/* 与 /admin/assets/* 静态资源直接输出(含 304 协商缓存) |
| admin.php | 管理后台 | ?action= 参数 + $controllerMap 映射表 | 登录校验、角色权限、操作级权限、插件后台页面分发均在此完成 |
| api.php | 开放 API | 无内置路由 | 核心不实现 API 逻辑,由 openapi 插件通过 api_dispatch 钩子接管;无插件接管时返回 404 JSON 提示 |
内核源码分为两块:core/lib/ 是功能类库(路由、数据库、钩子、插件、缓存、安全等),core/template/ 是模板引擎的组件拆分(编译器、解析器、继承、条件等)。所有类均由 core/lib/autoload.php 注册的自动加载器按需载入。
二、启动引导:bootstrap.php 逐段讲解
core/bootstrap.php 是所有入口的公共引导文件,按源码中的分节顺序执行:
- 输出缓冲(第 0 节):
ob_start()开启输出缓冲,保证后续任何位置都能设置 HTTP 头,避免 headers already sent 错误。 - 系统常量(第 1 节):定义路径类常量
ROOT_PATH、CORE_PATH、APP_PATH、STORAGE_PATH、LOG_PATH、UPLOADS_PATH与安装锁路径INSTALL_LOCK。 - 安装检查(第 2 节):
install.lock不存在时 302 跳转到/install.php,未安装的系统无法进入业务流程。 - 自动加载(第 3 节):引入
core/lib/autoload.php。该文件注册spl_autoload_register('autoload'),加载策略分三层:- PSR-4 命名空间映射:
BlogKit\Core\、BlogKit\Exception\、BlogKit\Template\、BlogKit\Models\、BlogKit\Services\、BlogKit\Controllers\Front\、BlogKit\Controllers\Admin\、BlogKit\Middleware\分别映射到core/lib/、core/lib/exceptions/、core/template/、app/下对应目录; - 无命名空间传统类名扫描:按
core/lib/、core/template/、app/Models/等固定路径逐个尝试{类名}.php; - 插件智能扫描:类名匹配
/(.+?)(Plugin|Extension|Hook)$/i时,到plugins/{小写前缀}/目录查找同名类文件。
- PSR-4 命名空间映射:
该文件末尾还会补齐路径常量的兜底定义,并引入 core/constants.php(业务常量:文章状态、用户角色、缓存 TTL、HTTP 状态码、CSRF 令牌有效期 CSRF_TOKEN_LIFETIME 等 20 组)。
- 核心类加载(第 4 节):显式 require
Config、Database、Model、Router、Security、ExceptionHandler、functions.php(全局辅助函数)与Template。 - 系统初始化(第 5 节):
Config::init()先加载core/config/下的文件配置(database.php、system.php、version.php),再从bk_config表读入全部运行时配置。数据库配置键为下划线命名(如cache_type),加载时会自动注册一份点语法别名(cache.type),新旧代码风格均可读取;debug_、cache_前缀的键同理。随后按site_timezone设置时区,并调用ExceptionHandler::register()注册统一异常处理器(须在Config::init()之后,以便读取 debug 配置)。 - 会话(第 6-7 节):会话文件存放于
storage/sessions/,Cookie 名固定为BKSID(独立于默认 PHPSESSID,避免同域部署的其他系统登录互踢),Cookie 参数为 HttpOnly、SameSite=Strict、HTTPS 下 Secure。会话采用 30 分钟滑动过期:last_activity超时后清除登录态并标记session_expired。 - 站点状态(第 7.1-7.2 节):
maintenance_mode开启时,非管理员访问返回 503 维护页;site_status为closed时非管理员同样 503,为private时未登录访客跳转登录页。 - CSRF Token(第 8 节):会话中无令牌时调用
Security::generateCsrfToken()生成(详见第八节)。 - 安全响应头(第 9 节):统一输出
X-Frame-Options: DENY、X-Content-Type-Options: nosniff、X-XSS-Protection、Referrer-Policy等响应头;HTTPS 下追加 HSTS。CSP 策略因主题模板存在大量内联样式与脚本,使用'unsafe-inline';同时每次请求生成随机 nonce 存入$_SESSION['csp_nonce']并注入模板变量csp_nonce。
三、路由系统
3.1 URL 到 action 的映射规则
前台路由集中在 core/lib/Router.php。核心是 $routeDefinitions 路由表,每行格式为:
// [路由名, 配置键, 默认 URL 模式, {id} 参数正则, 控制器, 方法]
['home_article', 'rewrite_article', 'article/{id}', '([a-zA-Z0-9_-]+)', 'Home', 'article'],匹配流程(resolveRoute 方法):
rewrite.enabled开启时,优先读取配置键对应的自定义伪静态规则(如rewrite_article),未设置则用默认模式;- 提取模式中的
{xxx}占位符,查内置占位符正则映射表(支持{id}、{slug}、{year}、{month}、{category_slug}、{username}等数十种);{id}特殊处理——优先使用路由行自带的参数正则(如分类路由用它兼容 slug 风格);未知占位符按([^\/]+)处理; - 对 URL 做全串正则匹配,全部捕获组按顺序作为位置参数传入控制器方法;
- 无占位符的路由做精确匹配。
在此之前,构造函数还有两条快速通道:/index.php/profile 直达 Auth::profile;?c=控制器&m=方法&id=值 的传统查询串格式直接映射(仅 id 作为位置参数,其余查询参数留在 $_GET 中由方法自行读取)。
dispatch() 分发时的完整顺序:
- 插件前台路由接管:
Plugin::matchFrontRoute($method, $path)按「先注册先得」顺序匹配已启用插件在 plugin.jsonroutes中声明的路由,命中即由插件控制器处理(插件侧写法见 06-插件开发指南 5.3 节); - 核心路由:加载
app/Controllers/Front/{Controller}Controller.php,实例化并调用方法; - 404 兜底:未命中时由
HomeController::notFound()渲染 404 页。
若以上均未命中,Router 还有一条兜底解析路径:按 / 切分 URL,第一段作控制器、第二段作方法、其余作参数——这保证了路由表遗漏时 index.php/控制器/方法 形式的 URL 仍可用。
3.1.1 路由表分组一览
$routeDefinitions 按控制器分组,当前共 24 条路由(摘录结构,完整清单见源码):
| 分组 | 代表路由 | 默认模式 | 说明 |
|---|---|---|---|
| Home 内容页 | home_article | article/{id} | {id} 正则兼容数字 ID 与 slug(含大写字母/下划线) |
| Home 列表页 | home_category / home_tag / home_page | category/{id} 等 | 分类、标签、独立页面 |
| Home 功能页 | home_tags / home_search / home_archives | tags、search、archives | 无参数精确匹配 |
| RSS | rss | rss | 另有 /feed 别名在 resolveRoute 中硬编码 |
| Auth 认证 | auth_login / auth_register / auth_logout 等 | login、register、logout 等 | 含找回密码、用户主页 user/{id} |
| 个人中心子页 | profile_articles / profile_comments 等 | profile/articles 等 | 九个子页均为无参数精确匹配 |
/profile 本身有特殊保护:无论其自定义重写规则如何设置,核心始终兜底将其映射到 Auth::profile,防止被站长误配置覆盖。
3.2 伪静态与 Router 的关系
伪静态分两层:
- 单条路由自定义:后台「系统设置 - 伪静态设置」(RewriteSettings 控制器)保存的
rewrite_*配置项,开启rewrite_enabled后逐条覆盖路由表默认模式。例如把rewrite_article设为archives/{year}/{postname},即可用日期式 URL 访问文章; - 全局重写规则:
rewrite.rules配置数组支持:参数风格的通用重写(applyRewriteRules方法),作为路由表未命中时的补充。
Nginx/Apache 只需把非静态请求统一转发到 index.php,URL 是否「伪静态」完全由 Router 层的配置决定。
3.3 如何新增一条路由
以新增 demo/{id} 映射到 Home::demo() 为例:
// 1. core/lib/Router.php 的 $routeDefinitions 数组中追加一行
// 配置键 rewrite_demo 允许站长在后台自定义该路由的伪静态形式
['home_demo', 'rewrite_demo', 'demo/{id}', '(\d+)', 'Home', 'demo'],
// 2. app/Controllers/Front/HomeController.php 中新增方法
// $id 由路由占位符 {id} 按捕获顺序传入
public function demo($id = 0) {
$articleModel = new ArticleModel();
$article = $articleModel->find((int)$id);
// ... 渲染模板或输出数据
}要点:路由行的参数正则只约束 {id} 占位符;若使用 {slug} 等其他占位符,正则取自内置映射表;参数可以有多个(如 {year}/{month} 会按顺序传入两个方法参数)。
四、数据库层
4.1 Database 类:PDO 封装
core/lib/Database.php 是单例模式的 PDO 封装。连接参数来自 core/config/database.php,连接选项为:异常模式报错、默认 FETCH_ASSOC 取值、关闭模拟预处理(EMULATE_PREPARES => false,SQL 注入防护的基础);字符集优先 utf8mb4,服务器不支持时自动降级 utf8。连接失败只记录日志并向页面输出通用错误信息(API 请求输出 JSON),不暴露任何数据库细节。
$db = Database::getInstance();
// 查询构建:全部使用预处理参数绑定
$row = $db->fetch("SELECT * FROM {$db->table('article')} WHERE id = ?", [$id]);
$list = $db->fetchAll("SELECT * FROM {$db->table('article')} WHERE status = ?", [1]);
// 结果缓存:第三参传 true 开启,第四参为过期秒数
// 缓存键为 db_fetch_ / db_fetchall_ 前缀 + SQL 与参数的 md5,需 cache.enabled 开启
$hot = $db->fetchAll("SELECT * FROM {$db->table('article')} ORDER BY views DESC LIMIT 10", [], true, 600);
// 写操作:表名自动加系统前缀,写后自动清理 db_* 缓存
$newId = $db->insert('article', ['title' => '标题', 'status' => 1]);
$db->update('article', ['status' => 2], ['id' => $newId]);
$db->delete('article', ['id' => $newId]);
// SQL 表达式:update 的值以 SQL: 开头时不作绑定,直接进入 SQL(值必须可信)
$db->update('article', ['views' => 'SQL:views + 1'], ['id' => $newId]);
// 事务
$db->beginTransaction();
try {
$db->insert('comment', ['content' => '内容', 'article_id' => 1]);
$db->update('article', ['comment_count' => 'SQL:comment_count + 1'], ['id' => 1]);
$db->commit();
} catch (Exception $e) {
$db->rollback();
}调试支持:debug.enabled 或 debug_slow_query_log 开启时,每次查询会记录耗时到内存查询日志(Database::getQueryLog() 读取,调试面板消费),超过 debug_slow_query_time 秒的查询写入慢查询日志。
// 查询日志与慢查询示例(需开启调试配置)
Database::clearQueryLog(); // 清空本请求的查询日志
$stmt = $db->query("SELECT * FROM {$db->table('config')}");
foreach (Database::getQueryLog() as $entry) {
// $entry 包含 sql / params / execution_time(毫秒) / timestamp
}注意:query($sql, $params, $cache, $expire) 签名中的缓存参数在本方法内不生效,结果缓存实际由 fetch/fetchAll 实现。另外任何写操作(insert/update/delete)都会通过 Cache::deleteByPattern('db_*') 清空全部数据查询缓存,是「整表失效」而非「单键失效」策略。
4.2 Model 基类:CRUD 封装
core/lib/Model.php 是所有业务模型的基类,构造时传入表名(不带前缀):
class ArticleModel extends Model {
public function __construct() {
parent::__construct('article'); // 实际表名为 bk_article
}
}
// 基类提供的通用方法
$model = new ArticleModel();
$model->find($id); // 按主键查单条(可指定主键名 $primaryKey)
$model->all(); // 全表
$model->get(['status' => 1], ['id' => 'DESC'], 10, 0); // 条件/排序/limit/offset
$model->count(['status' => 1]); // 计数
$model->insert($data); // 委托 Database::insert
$model->update($data, $where); // 委托 Database::update
$model->delete($where); // 委托 Database::delete使用约束:get()/count() 的条件数组只支持等值匹配,全部走预处理绑定;排序子句是字符串拼接,$orderBy 的键名必须来自受控白名单(禁止直接透传用户输入);limit/offset 以绑定参数方式进入 SQL。复杂查询(联表、聚合、IN 条件)应在业务模型子类中用 $this->db->fetchAll() 手写 SQL 完成,基类不做查询构建器式的链式封装。
五、钩子系统
5.1 注册与触发原理
钩子系统由两个类协作完成,形成「静态门面 + 注册表」结构:
core/lib/Hook.php:静态门面。定义了覆盖用户、文章、评论、页面、分类、标签、系统、上传、通知、邮件、后台、API、模板、安全、SEO、主题、插件、支付等领域的钩子名常量,并提供三个触发入口;core/lib/Plugin.php:注册表实际持有者。addHook($hook, callable $callback, $priority = 10)把回调按钩子名与优先级存入hooks[$hook][$priority][]数组并ksort排序,优先级数值越小越先执行。
数据流:内核代码调用 Hook::trigger(...) -> Plugin::triggerHook(...) -> 按优先级遍历回调并传入 $data -> 返回执行结果;整个过程记录钩子执行日志(受日志配置 enabled 与 enabled_categories 中 hook 分类的双重开关控制)。插件在 init() 中调用 $this->addHook() 完成订阅(插件侧写法见 06-插件开发指南 5.1 节)。
5.1.1 钩子命名规范与常量清单
钩子名采用 业务域_对象_动作_阶段 的下划线命名,常量名与字符串值一一对应,便于检索:
| 前缀(业务域) | 示例常量 | 示例钩子名 |
|---|---|---|
| USER_ / ADMIN_ | USER_LOGIN_AFTER / ADMIN_LOGIN_BEFORE | user_login_after / admin_login_before |
| ARTICLE_ / COMMENT_ | ARTICLE_CREATE_BEFORE / COMMENT_ADD_AFTER | article_create_before / comment_add_after |
| PAGE_ / CATEGORY_ / TAG_ | PAGE_VIEW_BEFORE 等 | page_view_before 等 |
| SYSTEM_ | SYSTEM_INIT / SYSTEM_CACHE_CLEAR | system_init / system_cache_clear |
| TEMPLATE_ | TEMPLATE_RENDER_BEFORE / TEMPLATE_ASSIGN | template_render_before / template_assign |
| PLUGIN_ / THEME_ | PLUGIN_ACTIVATE_AFTER / THEME_ACTIVATE_BEFORE | plugin_activate_after / theme_activate_before |
| FILE_ / EMAIL_ / CACHE_ | FILE_UPLOAD_AFTER / EMAIL_SEND_ERROR / CACHE_SET | 同名转换 |
完整的常量定义见 core/lib/Hook.php;哪些钩子在核心代码中实际被触发(而非仅定义),以 06-插件开发指南 5.1 节的「已验证触发的钩子清单」为准,监听优先选择该清单内的钩子。
5.2 三种触发方式
// 1. 普通触发:回调返回值按字符串拼接,返回拼接结果
Hook::trigger(Hook::ARTICLE_PUBLISH_AFTER, $article);
// 2. 带返回值触发:收集所有回调的返回值为数组(api.php 用它判断插件是否接管请求)
$results = Hook::triggerWithReturn('api_dispatch');
if (!empty($results)) {
exit; // 有插件接管,本次请求已由插件处理完毕
}
// 3. 过滤器:$data 依次传入各回调,每个回调的返回值作为下一个回调的输入
$content = Hook::filter('article_content_render', $content);配套方法:Hook::hasCallback($hookName) 判断某钩子是否有监听者。此外 core/lib/HookTrigger.php 提供了按业务语义命名的静态快捷封装(如 HookTrigger::systemInit()),内核控制器中多用它触发钩子。
5.3 钩子在内核与插件间的桥梁作用
内核通过钩子把扩展点暴露给插件,典型桥接模式有三类:
- 数据注入:模板引擎构造时通过
Hook::triggerWithReturn(Hook::TEMPLATE_NOTIFICATION_COUNT, $userId)向通知插件询问未读数,通过Hook::TEMPLATE_PLUGIN_FLAGS收集各插件的功能标记并注入为模板变量——内核只定义「问题」,答案由启用中的插件给出,插件未安装时取默认值(0 或 false); - 流程接管:api.php 的
api_dispatch钩子允许插件整体接管请求;front_profile_page、front_interaction_action等钩子的回调返回 true 即跳过核心默认处理; - 结果过滤:
article_content_render、search_results_filter、admin_config_collect_custom等过滤器钩子让插件修改内核产出的数据。
各入口中,只有 api.php 在引导阶段主动触发 system_init 钩子;前台与后台依赖具体业务位置(如模板变量注入、控制器动作前后)触发各自的钩子。
六、模板引擎内部
6.1 组件拆分
core/template/ 下,Template.php 是协调器,其余为职责单一的子组件:
| 文件 | 职责 |
|---|---|
| Template.php | 变量管理、站点/主题配置加载、子组件编排、渲染入口 |
| TemplateParser.php | 全部标签的解析管线(30 余个解析阶段按固定顺序执行) |
| TemplateCompiler.php | 编译结果缓存(读写与失效判断) |
| TemplateInheritance.php | 模板继承与 {block} 块 |
| TemplateCondition.php | 复杂条件标签 |
| TemplateUrl.php | {url:...} 类 URL 生成 |
| TemplateOutput.php | 输出辅助 |
| TemplateFilter.php | 变量过滤器 |
| TemplateExtension.php | 扩展标签注册 |
Template 构造函数完成几件关键初始化:模板根目录默认取 themes/{default_theme}/templates/(无该子目录时回退主题根目录),搜索路径包含模板根目录与其下 common/;随后注入一批框架级模板变量——config(全部站点配置)、登录用户的 total_notifications(经 template_notification_count 钩子询问插件)、插件功能标记(interaction_enabled、notification_enabled 等)、csrf_token(受 security_csrf_protection 开关控制)、csp_nonce。
6.2 渲染流程与标签解析管线
$template = new Template(); // 默认指向当前主题模板目录
$template->assign('articles', $list);
$template->display('index.html'); // 等价于 echo $template->render('index.html')render() 的执行路径:loadTemplate($name) 按「模板名 -> 模板名.html -> common/模板名 -> common/模板名.html」的顺序在搜索路径中定位文件;命中后(编译缓存未命中时)先做继承解析(parseInheritance + parseBlocks),再交给 TemplateParser::parse()。
标签管线按固定顺序执行约 30 个解析阶段,可归为六组:
| 顺序 | 解析组 | 覆盖标签(举例) |
|---|---|---|
| 1 | 结构与复用 | {cache} 区块、{macro} 宏、{assign}、{include} |
| 2 | 内置与安全 | {year}、{current_url}、{csrf_field}、{plugin_hook} |
| 3 | 配置与条件 | {config}、{site.xxx}、{if} 及其变体(in_array/isset/empty/equal/变量比较) |
| 4 | 循环与代码 | {loop}、{loop.xxx} 再访问、{php} |
| 5 | 页面组件 | {field}、{pagination}、{breadcrumb}、{url:xxx}、{top_badge}、{func:xxx} |
| 6 | 收尾 | 变量输出 `{$var |
管线顺序意味着后解析的标签不会出现在先解析阶段产生的文本中,扩展新标签时应参考 05-主题开发指南 的标签清单并在解析器中选择合适的插入位置。
6.3 编译缓存机制
编译缓存由 TemplateCompiler 负责,开关逻辑在 Template 构造函数中:后台「缓存功能开关」cache_enabled 为 1 且 template_cache.enabled(默认开启)时启用。机制细节:
- 缓存目录:
storage/cache/templates/;缓存文件名为md5(模板文件绝对路径) . '.php'; - 有效性判断:缓存文件的 mtime 必须晚于或等于模板文件 mtime(
isCompiled比较),因此编辑模板文件保存后,下一次请求即自动重新编译,无需手动清理; - 缓存内容是「解析后的模板文本」而非 PHP 代码,运行时不再执行标签替换,直接进入后续输出环节;
cache_enabled关闭时每次请求都实时解析全部标签,便于主题调试。
另有与编译缓存并行的区块缓存:模板中 {cache ttl="600" key="sidebar"}...{/cache} 包裹的片段,其渲染结果以 md5(key).html 形式写入同一目录,TTL 内直接复用,适合侧栏等查询密集区块。
缓存失效途径有三:模板文件修改(mtime 比较自动失效)、后台清空缓存操作(调用 TemplateCompiler::clear() 删除目录下全部 .php 编译文件)、cache_enabled 开关切换。模板标签语法的完整用法见 05-主题开发指南。
七、缓存系统
7.1 驱动类型与配置
core/lib/Cache.php 为单例缓存门面,支持两种驱动:
- file(默认):每条缓存一个
.cache文件,内容为serialize后的数组(值、过期时间、创建时间、压缩标记);后台开启cache_compression后使用 gzip 压缩存储; - redis:
cache_type配置为redis时启用,连接参数在redis配置组(host/port/password/timeout);连接失败自动降级为文件缓存并记录错误。
主要配置键与后台设置的对应关系:
| 配置键 | 含义 | 默认值 |
|---|---|---|
cache_enabled | 缓存总开关(关闭时数据缓存与模板编译缓存均不生效) | 0 |
cache_type | 驱动类型 file/redis | file |
cache_path | 缓存目录 | storage/cache |
cache_expire | 默认过期秒数 | 3600 |
cache_size_limit | 文件缓存容量上限(MB) | 100 |
cache_key_prefix | 缓存键前缀 | blog_ |
cache_compression | 是否 gzip 压缩 | 0 |
cache_history_days | 监控统计数据保留天数 | 7 |
7.2 缓存键规范与失效
所有键经 generateCacheKey() 归一化:最终键 = 前缀 + 命名空间_ + md5(完整键)。键中含冒号(如 article:list:home)时,冒号前的部分会转为下划线形式作为命名空间前缀,便于按模式批量清理。推荐的键写法是「业务域:对象:标识」,例如 article:detail:123。
Cache::setCache('article:detail:123', $article, 600); // 设置
$article = Cache::getCache('article:detail:123'); // 读取(未命中返回 null)
Cache::deleteCache('article:detail:123'); // 单键删除
// 标签失效:写入时附带标签,之后可整组失效
Cache::setCache('article:detail:123', $article, 600, ['article']);
Cache::invalidateTag('article'); // 删除所有带该标签的缓存文件缓存的过期采用惰性删除:读取时发现已过期即删除文件并返回未命中。容量超过 cache_size_limit 时按 LRU 策略(按文件 mtime 从旧到新)清理最旧文件,清理策略固定为 LRU,后台展示的清理策略项仅为说明。数据库查询缓存使用 db_ 开头的键,任何数据写操作会触发 deleteByPattern('db_*') 全量清理(见第四节)。缓存命中/写入统计在开启监控(cache_monitoring_enabled)时周期性写入 bk_cache_stats 表,供后台缓存页查看命中率与健康状态;插件可订阅 cache_clear_before/after 等钩子参与缓存生命周期。
7.3 批量操作、统计与健康检查
Cache 门面除单键操作外还提供成组能力(均受 cache_batch_operation 开关控制):
| 方法 | 用途 |
|---|---|
batchSet($items) | 批量写入,$items 格式为 ['键' => ['value' => 值, 'expire' => 秒]] |
batchGet($keys, $default = null) | 批量读取,返回 键 => 值 数组 |
batchDelete($keys) | 批量删除 |
deleteByPattern($pattern) | 按模式删除(仅文件驱动,glob 匹配 键*.cache) |
getStats() | 命中/未命中/写入/删除计数、命中率、缓存目录体积 |
getHealthStatus() | 驱动连通性、缓存目录可写性、磁盘占用、容量使用率的综合健康报告 |
监控开启时,统计快照会写入 bk_cache_stats 表(首次使用时自动建表,含 hits/misses/sets/deletes/cache_size/hit_rate 字段),并按 cache_history_days 清理过期记录;该表数据供后台缓存管理页绘制命中率趋势。上述全部静态便捷方法(Cache::getCache/setCache/deleteCache/clearCache 等)内部均转发到单例实例,插件与业务代码可任意选用。
八、安全组件
core/lib/Security.php 提供贯穿全站的安全原语,各入口与中间件在其上组装防护。
8.1 CSRF Token 的生成与校验
- 生成(
generateCsrfToken()):令牌在CSRF_TOKEN_LIFETIME(7200 秒,定义于core/constants.php)内复用——同一页面渲染多个表单不会互相作废;生成优先使用random_bytes(32)(CSPRNG),降级openssl_random_pseudo_bytes,最后兜底uniqid + mt_rand;存入$_SESSION['csrf_token']与$_SESSION['csrf_token_time']。bootstrap 在会话就绪后统一生成; - 校验(
validateCsrfToken($token, $expiration = null)):依次执行——后台security_csrf_protection开关为 0 时直接放行(默认 1 开启);空值与非字符串拒绝;会话无令牌拒绝;hash_equals常时比较防时序攻击;过期检查; - 输出(
getCsrfField()):返回<input type="hidden" name="csrf_token" value="...">,开关关闭时返回空字符串;模板中亦可使用{csrf_field}标签。
8.2 输入过滤与校验
filterXss($input, $flags, $encoding):对字符串做htmlspecialchars(默认ENT_QUOTES | ENT_SUBSTITUTE,UTF-8),数组与对象递归处理;受xss_protection配置开关控制;sanitizeInput($input):先trim再递归 XSS 过滤;- 校验族:
isValidEmail/isValidUrl/isValidIp/isValidInt/isValidFloat/isValidLength基于filter_var实现; filterSql()已标记弃用并触发E_USER_DEPRECATED,SQL 注入防护的正确途径是 PDO 预处理(见第四节,EMULATE_PREPARES已关闭);getClientIp()按HTTP_CF_CONNECTING_IP、X-Forwarded-For等头优先级取值并做 IP 格式校验,多 IP 时取第一个——部署在不可信代理之后时需注意该头可被伪造。
8.3 安全响应头与会话防护的内部要点
bootstrap 每次请求统一输出以下响应头:
| 响应头 | 取值 | 防护目标 |
|---|---|---|
| X-Frame-Options | DENY | 点击劫持(禁止被 iframe 嵌套) |
| X-Content-Type-Options | nosniff | MIME 嗅探 |
| X-XSS-Protection | 1; mode=block | 旧浏览器 XSS 过滤器 |
| X-Permitted-Cross-Domain-Policies | none | 跨域策略文件加载 |
| X-Download-Options | noopen | 下载文件直接打开 |
| Referrer-Policy | strict-origin-when-cross-origin | 引用来源泄露 |
| Strict-Transport-Security | max-age=31536000; includeSubDomains(仅 HTTPS) | 强制 HTTPS |
| Content-Security-Policy | 脚本/样式 'unsafe-inline' 等指令 | 资源加载白名单 |
会话与请求判别类辅助方法:会话 Cookie 属性(HttpOnly/SameSite=Strict/HTTPS Secure)、30 分钟滑动过期均实现于 core/bootstrap.php(见第二节第 7、10 步);Security::isAjaxRequest() 依据 X-Requested-With 头判断、isPostRequest()/isGetRequest() 判断请求方法,控制器中常用于分支处理。后台另有 admin_ip_whitelist_enabled 开关控制的 IP 白名单(core/lib/IpWhitelist.php,支持可信代理配置),在 admin.php 入口于路由分发前拦截。表单处理的完整实践(token 注入、POST 校验、失败处理)见 06-插件开发指南 的示例代码。
九、源码导读地图
按「想改什么功能,先看哪个文件」整理的速查表(路径均相对系统根目录):
| 目标 | 源码位置 |
|---|---|
| 调整引导流程、会话、安全响应头 | core/bootstrap.php |
| 新增/修改前台路由、伪静态占位符 | core/lib/Router.php($routeDefinitions 与 resolveRoute) |
| 后台新增功能页、权限码、操作级校验 | admin.php($controllerMap、SUB_ACTION_PERMISSION_MAP)+ 对应 app/Controllers/Admin/ 控制器 |
| 数据库连接、查询缓存策略、慢查询日志 | core/lib/Database.php |
| 通用 CRUD 封装调整 | core/lib/Model.php |
| 新增钩子定义点、调整触发语义 | core/lib/Hook.php(常量)+ core/lib/HookTrigger.php(快捷封装) |
| 插件加载、生命周期、前台/后台路由接管、ACL | core/lib/Plugin.php;插件基类在 core/lib/PluginBase.php |
| 模板编译缓存策略 | core/template/TemplateCompiler.php + core/template/Template.php(构造函数开关逻辑) |
| 新增/调整模板标签解析 | core/template/TemplateParser.php(parse() 管线中定位插入点) |
| 模板继承与块机制 | core/template/TemplateInheritance.php |
| 缓存驱动、键规范、标签失效、监控 | core/lib/Cache.php |
| CSRF、XSS 过滤、输入校验 | core/lib/Security.php |
| 运行时配置加载与别名机制 | core/lib/Config.php + core/config/ 目录 |
| 统一异常处理与 404/403 页 | core/lib/ExceptionHandler.php + core/lib/exceptions/ |
| 业务常量(状态码、TTL、上传限制等) | core/constants.php |
| 自动加载规则 | core/lib/autoload.php |
| 全局辅助函数 | core/lib/functions.php |
| API 入口逻辑与插件接管点 | api.php |
| 主题静态资源服务与 304 缓存 | index.php(serveStaticFile()) |