钩子拿到的东西怎么用:文章页、列表页三处实战
钩子拿到的参数是引用还是副本,决定了改了算不算数。本文按 Article_Content、ViewList_Core、Post_Get 三个钩子列出各自能拿到什么,配三段可直接用的实战代码:文章页按分类取相关文章、正文自动生成目录、列表页取分类父子路径。最后记三个坑:漏写 & 改动静默失效、目录用 .= 会跑到文末、整页正则误伤评论区标题。
写这篇的起因很具体:我想给文章页自动生成一个目录,就是那种「一、二、三」的小导航。第一版写出来,目录下面还多带出了评论区的标题——因为我把整页 HTML 都塞进正则里了,不只是正文。
后来才发现,问题不在正则写错,而在我根本没搞清楚钩子给了什么。这篇就按这个顺序走:先弄明白手上拿到的是什么,再谈怎么用。
一、先搞清楚手上拿到的是什么
上一篇讲的是「能挂在哪」,这篇讲「挂上去之后手里有什么」。三列对照,第三列最关键:
| 我要干什么 | 挂哪个钩子 | 手上有什么 |
|---|---|---|
| 改正文 | Filter_Plugin_Article_Content | &$article(文章对象)+ &$content(正文 HTML 字符串,引用传参) |
| 改列表 | Filter_Plugin_ViewList_Core | &$type、&$pageBar、&$articles(文章数组)、&$select、&$order、&$limit |
| 改单个文章对象 | Filter_Plugin_Post_Get(在取文章时触发) | &$post,可以改里面的字段 |
记住一个点就够了:带 & 的是引用,改了才算数。函数签名里少写一个 &,钩子照样触发,代码照样不报错,但你的修改会凭空消失。这个坑我在第三节专门写。
二、实战一:文章页按分类取相关文章
需求:文章底部显示同分类的另外 3 篇,不算当前这篇。
function mytheme_related_articles(&$article, &$content) {
global $zbp;
if ($article->Status != 1) return; // 草稿、私密不处理
$cateId = (int)$article->Category->ID;
if ($cateId === 0) return;
// 找这个分类及其子分类
$ids = array($cateId);
foreach ($zbp->categorys as $c) {
if ((int)$c->ParentID === $cateId) $ids[] = (int)$c->ID;
}
$ids = implode(',', $ids);
// 排除自己,按时间倒序
$list = $zbp->GetList(
'*',
'log_CateID IN (' . $ids . ') AND log_Status = 1 AND log_ID <> ' . (int)$article->ID,
array('log_PostTime' => 'DESC'),
3,
null
);
if (empty($list)) return;
$html = '<div class="related"><h3>这个分类下还有</h3><ul>';
foreach ($list as $p) {
$html .= '<li><a href="' . $p->Url . '">' . htmlspecialchars($p->Title) . '</a></li>';
}
$html .= '</ul></div>';
$content .= $html; // 注意是 .= ,不是 =
}$zbp->categorys 是内存里的分类缓存,遍历它找子类比再查一次数据库快得多。第一次用这个的时候,我居然在循环里又 GetList('*', 'cate_ID = ...'),一页文章 20 篇,就多跑了几十次查询。
三、实战二:正文自动生成目录
就是我开头说的那个需求。关键是只在正文上做正则,别碰整页:
function mytheme_toc(&$article, &$content) {
if (stripos($content, '<h2') === false) return;
preg_match_all('/<h2[^>]*>(.*?)<\/h2>/is', $content, $m);
if (count($m[1]) < 3) return; // 少于 3 个小标题不值得生成
$toc = '<nav class="toc"><strong>本文目录</strong><ol>';
foreach ($m[1] as $i => $title) {
$title = trim(strip_tags($title));
$id = 'sec-' . ($i + 1);
$toc .= '<li><a href="#' . $id . '">' . htmlspecialchars($title) . '</a></li>';
$content = str_replace($m[0][$i], '<h2 id="' . $id . '">' . $m[1][$i] . '</h2>', $content);
}
$toc .= '</ol></nav>';
$content = $toc . $content;
}这里同样要注意:$content = $toc . $content 是在引用参数上重新赋值,能生效;写 $content .= $toc 就是把目录放到了正文最后,我就干过这个事,目录跑到了文末。
四、实战三:列表页取当前分类的父子关系
需求:分类页顶部显示「父分类 → 当前分类」的层级路径。挂 Filter_Plugin_ViewList_Core,从 $type 和 $pageBar 里拿:
function mytheme_list_breadcrumb(&$type, &$pageBar, &$tpl, &$articles, &$select, &$order, &$limit, &$option) {
global $zbp;
if ($type != 'category') return;
$cate = $zbp->GetCategory((int)$zbp->CurrentMetas);
// 更常见的写法是直接读全局
$cate = isset($GLOBALS['cate']) ? $GLOBALS['cate'] : $cate;
if (!$cate || !$cate->ID) return;
$path = array();
$tmp = $cate;
while ($tmp && $tmp->ID) {
array_unshift($path, $tmp);
$tmp = $tmp->ParentID ? $zbp->GetCategory($tmp->ParentID) : null;
}
foreach ($path as $c) {
echo '<a href="' . $c->Url . '">' . htmlspecialchars($c->Name) . '</a> > ';
}
}注意这里用的是 ParentID 往上爬,不是只拿一级父类。下面这个坑写得很清楚。
五、我踩过的三个坑
坑一:少写一个 &,改了没用还不报错。函数签名写 function foo($article, $content),里面怎么改都不影响页面。我第一次排查了半小时,一度以为钩子没挂上,最后发现是少个 &。
坑二:在 Article_Content 里再查数据库。一篇文章触发一次看不出问题,列表页 20 篇、每篇都查一次,页面直接从 200ms 掉到 1.5 秒。能用 $zbp 内存缓存就别查。
坑三:只拿一级父分类。分类层级是三层的时候,我的面包屑只显示「技术片段 > zblog」,漏掉中间层。后来改成 while 一直往上爬,才算完整。
六、最后
钩子这东西不难,难在「手上有什么」这一步没搞清楚就动手,写出来的代码看着对,一运行就是没效果或者重复一堆。
如果你正在写的钩子也出现了「代码跑了但页面没变」,把你挂的钩子、函数签名、想实现的效果三句话发过来,我对着表帮你对一遍,多半是 & 或者判断层级的地方。下一篇我会讲一个更实际的东西:把这几段代码收成一个真正能装的插件,从 plugin.xml 到启用,走一遍完整流程。