分类父子关系怎么取,URL 到底怎么生成

· zblog · 阅读 38
摘要

Z-Blog 的分类对象看着字段挺全,但一半是魔术属性,每次访问现算。分清哪些是存的、哪些是算的,才不会写出「改了没生效」的代码。这篇把 Level、Root、Parent、AllCount 逐条核了一遍,顺带说清分页地址为什么不能手拼。

写钩子那个系列收尾的时候(《给插件加个后台页:Config 存条数与标题》那篇)我说过想聊分类和 URL,后来真去做面包屑,踩了几个坑,今天记下来。

先说结论:Z-Blog 的分类对象看着字段挺全,但有一半是魔术属性,每次访问现算,不是库里存的字段。分不清哪些是存的、哪些是算的,就会写出「改了没生效」或者「循环里查了三十次数据库」的代码。

Level 是从 0 开始数的

我一开始写 for ($i = 1; $i < $c->Level; $i++) 想缩进,结果顶级分类输出空循环,二级只缩一层。翻源码,Level 在 __get 里返回 GetDeep($this),而那个方法第一行就是:

if ($object->ParentID == 0) {
    return $deep;   // 默认 0
}

顶级分类的 Level 是 0,不是 1。判断「是不是顶级」写 $c->Level == 0,别写 == 1。

另外 Level 每取一次都要沿父链递归往上走,是 O(深度)。列表页对几十个分类挨个取,影响不大;但在循环里同时取 Root 和 Parent,就是三层递归叠着跑。真在意性能,循环前先算好存进数组。

SubCategories、ChildrenCategories,还有个拼错的

属性含义是否含子孙
SubCategories直接子分类否
SubCategorys同上,拼写错误的别名否
ChildrenCategories所有子孙分类,扁平数组是

SubCategorys 是历史遗留的错拼,源码里用引用指向 SubCategories,改一个另一个跟着变。新代码用 SubCategories 就行。

ChildrenCategories 是扁平的,不含层级信息。想要带层级输出一棵树,得自己递归 SubCategories,别指望它。

RootID 是存的,Root 是算的

这对最容易搞混:

  • RootID 是真实字段,存在表里,保存分类时由 Save() 里的 GetRoot($this->ParentID) 算好写进去。可以直接当 SQL 条件用。
  • Root 是魔术属性,返回 $zbp->categories_all[$this->RootID] 这个对象。用来取名字、取链接可以,别塞进查询条件。

同理 Parent 返回对象,ParentID 才是字段。写查询时永远用带 ID 后缀的那个。

Count 和 AllCount 差在哪

做分类列表常要显示「本篇数」。Count 是字段,只统计挂在本分类下的文章,不含子分类。想让父分类显示「含子孙总数」,用 AllCount:

if ($name == 'AllCount') {
    $i = $this->Count;
    foreach ($this->ChildrenCategories as $c) {
        $i += $c->Count;
    }
    return $i;
}

所以 AllCount 是每次现算的,遍历一遍所有子孙。分类树不深就无所谓,深的话同样建议缓存。

URL 到底怎么生成的

取 $c->Url 时,__get 里走 UrlRule,把 {%id%} 和 {%alias%} 替换掉再 Make()。规则来自你后台「静态管理中心」里的分类正则,本站是:

{%host%}{%alias%}_{%page%}/

所以顶级分类是 /zixun/,第二页是 /zixun_2/。第一页不带页码,页码为 1 时那段会被整段吃掉——这是 UrlRule 的行为,不是分隔符没配对。

这里有个我踩过的坑:手拼分页链接写成 $c->Url . '?page=2',伪静态模式下这个参数不生效,永远停在第 1 页。正确做法是用 PageBar,或者手动 new UrlRule($zbp->option['ZC_CATEGORY_REGEX']) 再把 {%page%} 填进去。别手工拼分类的分页地址。

还有 alias 为空的情况。源码里是:

$u->Rules['{%alias%}'] = rawurlencode_without_backslash(
    $this->Alias == '' ? $this->$backAttr : $this->Alias
);

别名空着时会回退取 ZC_ALIAS_BACK_ATTR 指定的字段,默认是 Name。所以中文分类名也能生成地址,只是会被 URL 编码成一串 %E6%8A%80...,难看但能访问。想让地址干净,就把别名填上。

面包屑怎么写

拼起来就三行,FindParents 已经封装好了:

$crumbs = array();
if ($c->ParentID > 0) {
    $c->IsParents($c->ParentID); // 或直接用下面的数组
}
$parent = $c->Parent;
while ($parent) {
    array_unshift($crumbs, $parent);
    $parent = $parent->Parent;
}
$crumbs[] = $c;

注意 Parent 到顶级时返回 null(源码里直接 return;),所以 while ($parent) 能正常收尾,不会死循环。

小结

记三条:

  1. Level 从 0 起,顶级是 0;
  2. 查条件只用带 ID 后缀的字段,Parent / Root / Level / AllCount 都是现算的;
  3. 分页地址别手拼,交给 UrlRule 或 PageBar。

下一篇想写伪静态本身——规则逐条拆开,{%page%} 为 1 时那一段为什么整段消失、尾斜杠什么时候该带,Nginx 和 Apache 两套配置各写一份。