主题和插件加载后,可以直接使用纸鱼博客提供的全局函数和核心类。本文只介绍适合扩展开发的公共接口;未在本文列出的内部方法不保证版本兼容。
输出与安全
e() HTML 转义
将用户输入、文章标题、设置值和 URL 输出到 HTML 前,使用 e():
<h1><?= e($title) ?></h1>
<a href="<?= e($url) ?>">查看</a>只有系统已经渲染过的 $contentHtml 可以直接输出。不要对用户输入使用 echo,也不要把未经校验的内容拼接到 <script> 中。
md() Markdown 渲染
$html = md($markdown);返回 HTML。是否允许原始 HTML 由后台“站点设置 → 文章与内容”控制。
URL 和资源
url_to() 站内链接
$postUrl = url_to('/post/hello-world');
$loginUrl = url_to('/login');函数会自动适配伪静态开关和子目录部署。不要手动拼接 index.php。
api_url() API 地址
$url = api_url('/posts?page=1');根据站点是否启用伪静态,生成 /api/posts?... 或 /index.php?p=api/posts&...。
absolute_url() 绝对地址
$url = absolute_url('/post/hello-world');根据 runtime/config.php 中的 site_url 生成绝对 URL,适合 RSS、邮件、Open Graph 和 Webhook。传入完整的 http:// 或 https:// 地址时会原样返回。
asset_url() 和 theme_asset_url()
<link rel="stylesheet" href="<?= e(asset_url('/css/app.css')) ?>">
<script src="<?= e(theme_asset_url('example', 'assets/app.js')) ?>" defer></script>asset_url() 用于 public/ 下的系统资源,通常生成 /public/... 形式的 URL;当站点根目录已经指向 public/ 时省略前缀,生成 /...。theme_asset_url() 用于主题目录资源,生成 /themes/{主题名}/... 形式的 URL。资源路径不能包含 ..。启用伪静态时,Nginx 的 location / 必须把不存在的路径交给项目根目录 index.php;Apache 请启用随发行包提供的 .htaccess。部署配置参见安装说明。
模板渲染
render()
渲染主题模板,主题文件优先,系统模板兜底:
echo render('notice', ['message' => '你好']);对应文件为当前主题的 notice.php。传入的数据会合并到当前模板上下文。
render_partial()
用于渲染局部模板:
<?= render_partial('post-card', ['post' => $post]) ?>get_header() 和 get_footer()
主题页面通常使用这两个函数加载公共头部和页脚:
<?php get_header(); ?>
<main>页面内容</main>
<?php get_footer(); ?>站点和主题设置
settings()
读取后台“站点设置”:
$siteName = settings('site_name', '纸鱼博客');
$all = settings();传入键名时返回单个值,不传参数时返回全部设置。返回值可能是字符串、数字或数组,请根据实际类型处理。
theme_value()
读取当前主题的设置值,未保存时使用 theme.json 中的默认值:
$color = theme_value('accent_color', '#2563eb');site_name()
读取站点名称:
echo e(site_name());nav_items()、widget_items() 和 friend_links()
分别读取前台可见的导航、组件和友情链接:
foreach (nav_items() as $item) {
echo '<a href="' . e($item['url']) . '">' . e($item['label']) . '</a>';
}widget_items($area) 的默认区域是 sidebar,自定义区域名只能使用小写字母、数字、下划线和连字符。内置组件类型为 categories、tags、recent_posts、hot_posts、recent_comments、recent_micro_statuses(最新微语,仅公开内容)和 custom(自定义标题 + 内容),组件在后台“组件管理”中配置。
用户和权限
current_user()
获取当前登录用户,未登录时返回 null:
$user = current_user();
if ($user !== null) {
echo e($user['username'] ?? '');
}用户数组包含数据库字段,可能包含敏感信息。只输出明确需要的字段,不要把整个数组发送到前台或写入日志。
is_logged_in() 和 is_admin()
if (is_logged_in()) {
// 已登录
}
if (is_admin()) {
// 当前用户是管理员
}这两个函数适合控制模板展示。插件后台接口仍必须使用后台权限中间件或 Auth::requireCapability() 做服务端校验。
日期和表单
format_date()
把日期或时间字符串格式化为站点时区下的日期:
echo e(format_date($post['published_at'] ?? null));
echo e(format_date($post['published_at'] ?? null, 'yyyy-MM-dd HH:mm'));csrf_field() 和 csrf_token()
用于站内表单和带状态变更的请求:
<form method="post">
<?= csrf_field() ?>
<button type="submit">保存</button>
</form>自定义 POST 操作必须验证 CSRF token,不能只依赖登录状态。
Action 和 Filter
Action
Action 用于在某个事件发生后执行代码,不改变原始数据:
add_action('after_post_published', function (array $post): void {
error_log('已发布:' . ($post['title'] ?? ''));
}, 10);Filter
Filter 接收当前值并返回修改后的值:
add_filter('frontend_meta', function (array $meta, array $context): array {
$meta['robots'] = 'noindex';
return $meta;
}, 10);插件开发优先使用插件上下文的 $ctx->on() 和 $ctx->filter(),系统会在停用插件时自动移除这些钩子。完整事件列表请参阅插件开发。
数据库访问
纸鱼博客通过 PDO 提供 DB 类,模板和插件可以使用以下方法:
$posts = DB::fetchAll(
'SELECT id, title FROM posts WHERE status = ? ORDER BY id DESC LIMIT 10',
['PUBLISHED']
);
$post = DB::fetchOne('SELECT * FROM posts WHERE id = ?', [$id]);
$count = DB::value('SELECT COUNT(*) FROM posts WHERE status = ?', ['PUBLISHED']);常用方法:
方法 | 返回值 |
|---|---|
|
|
| 多行数组 |
| 单行数组或 |
| 第一列的值 |
| 受影响行数 |
| 最近插入 ID |
| 事务回调结果 |
| 当前是否处于受管事务 |
| 注册提交后回调;事务回滚时丢弃 |
始终使用参数绑定,禁止把用户输入直接拼接进 SQL。插件自己的数据优先使用 $ctx->getData() 和 $ctx->setData(),避免依赖 纸鱼博客内部表结构。
核心类别名
以下类可以直接使用短类名,也可以使用完整命名空间:
别名 | 对应类 | 常用用途 |
|---|---|---|
|
| 数据库访问 |
|
| URL 生成 |
|
| 读取核心配置(优先 |
|
| 查询主题、模板和主题设置 |
|
| 底层钩子管理 |
通常不需要直接调用 Hooks,使用 add_action()、add_filter() 或插件上下文即可。
缓存
需要缓存只读数据时可以使用 Pafish\Core\Cache:
use Pafish\Core\Cache;
$value = Cache::get('example:key');
Cache::set('example:key', ['ready' => true], 300);
Cache::delete('example:key');
// 查询结果应使用回填接口,避免失效后写回旧查询结果。
$rows = Cache::remember('example:list', static function (): array {
return \Pafish\Core\DB::fetchAll('SELECT id, name FROM categories ORDER BY sort_order, id');
}, 300);缓存键只能使用字母、数字、点、冒号、下划线、连字符和 /,不能包含 ..。缓存不是持久数据,不能把它当作订单、设置或用户信息的唯一存储。
remember() 将读取、回源和回填连在一起:失效或直接 set 发生在查询期间时,不再发布该查询快照;已经开始的请求仍可能返回自己的旧结果。同一缓存项的并发 miss 在约 250ms 等待预算内共用一次加载,超时直接回源但不写缓存。文件锁只用于本机文件缓存,不等于跨节点一致性保证。现有 get()/set() 接口保留,分开调用无法得知外部查询何时开始,因而不提供相同的回填保护。
事务内 remember() 直接执行加载,不读取或发布共享缓存;写操作的 delete()/clearPrefix() 必须使用 DB::transaction 管理最外层提交。不要缓存认证凭据、密码解锁状态、待审个人回显或含私密数据的整页。导航、友链、组件、分类树和最新公开微语已接入同一回填接口;原 cache_list() 仍不缓存空数组,避免改变安装播种行为。
Cache::stats() 返回当前请求的命中、读取未命中、回源次数/耗时、回填等待/超时、拒绝旧回填、写入、错误与失效扫描计数;Cache::resetStats() 用于专项测量。双重检查可能产生两次 miss,不能直接把 misses 当作独立请求数。统计不持久化,不包含 SQL、缓存键、用户或内容;当前没有后台指标页。
clearPrefix('example:') 以字符串前缀做逻辑失效,不遍历值文件;后续 get/remember 通过同一稳定锁中的失效标记拒绝旧值。不要以“文件仍在”判断是否失效,也不要直接 include 缓存文件。匹配键重新加载时复用原路径;普通失效不立即回收磁盘,Cache::clear() 才扫描并删除值文件。此前缀行为不要求以点或冒号结尾。
前缀与精确删除标记合计最多 256 条;超过上限安全轮换缓存代次,让全部值重新加载,避免动态插件键无限堆积标记。更宽前缀会合并其覆盖的旧标记。该限制不是最多允许 256 个缓存键。升级遇到旧格式或损坏状态时安全冷启动,不涉及数据库迁移;发布应停止旧 worker 后切换代码,避免两版实现同时写同一目录。
开发原则
不要直接读取或修改
$_POST后拼接 SQL;使用参数绑定和严格校验。不要输出完整用户对象、配置数组或插件设置中的密钥。
不要直接修改系统模板和核心文件,优先使用主题覆盖与插件钩子。
自定义前台 HTML、脚本和外部请求都要考虑转义、CORS、超时和权限。