钩子拿到的东西怎么用:文章页、列表页三处实战

· zblog · 阅读 38
摘要

钩子拿到的参数是引用还是副本,决定了改了算不算数。本文按 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> &gt; ';
    }
}

注意这里用的是 ParentID 往上爬,不是只拿一级父类。下面这个坑写得很清楚。

五、我踩过的三个坑

坑一:少写一个 &,改了没用还不报错。函数签名写 function foo($article, $content),里面怎么改都不影响页面。我第一次排查了半小时,一度以为钩子没挂上,最后发现是少个 &。

坑二:在 Article_Content 里再查数据库。一篇文章触发一次看不出问题,列表页 20 篇、每篇都查一次,页面直接从 200ms 掉到 1.5 秒。能用 $zbp 内存缓存就别查。

坑三:只拿一级父分类。分类层级是三层的时候,我的面包屑只显示「技术片段 > zblog」,漏掉中间层。后来改成 while 一直往上爬,才算完整。

六、最后

钩子这东西不难,难在「手上有什么」这一步没搞清楚就动手,写出来的代码看着对,一运行就是没效果或者重复一堆。

如果你正在写的钩子也出现了「代码跑了但页面没变」,把你挂的钩子、函数签名、想实现的效果三句话发过来,我对着表帮你对一遍,多半是 & 或者判断层级的地方。下一篇我会讲一个更实际的东西:把这几段代码收成一个真正能装的插件,从 plugin.xml 到启用,走一遍完整流程。