面包屑需从当前栏目向上逐层查询父级,用while循环+array_unshift()构建逆向路径;避免递归防栈溢出,SQL自连接适用于深度稳定的场景,渲染时需结构化输出并安全处理链接与分隔符。

PHP 中用递归获取栏目面包屑的典型结构
面包屑本质是「当前栏目 → 父栏目 → 祖父栏目」的逆向路径,必须从当前 $cat_id 向上逐层查 parent_id,不能正向遍历。常见错误是写成“从顶级开始往下找”,结果死循环或漏级。
假设栏目表 category 有字段:id、name、parent_id(根栏目为 0 或 NULL):
function getBreadcrumb($cat_id, $pdo) {
$crumb = [];
while ($cat_id > 0) {
$stmt = $pdo->prepare("SELECT id, name, parent_id FROM category WHERE id = ?");
$stmt->execute([$cat_id]);
$row = $stmt->fetch();
if (!$row) break;
array_unshift($crumb, ['id' => $row['id'], 'name' => $row['name']]);
$cat_id = (int)$row['parent_id'];
}
return $crumb;
}
- 用
while而非递归函数,避免 PHP 栈溢出(尤其栏目深度 > 10) -
array_unshift()是关键:每次把父级插到开头,自然形成「首页 → A → B → 当前」顺序 - 必须判断
!$row中断,防止因数据异常(如parent_id指向不存在 ID)陷入无限循环
MySQL 自连接一次性查出完整路径(适合中等深度)
当栏目树深度稳定(如 ≤6 级),用 SQL 自连接比 PHP 循环更高效,减少查询次数。但注意 MySQL 8.0+ 才原生支持递归 CTE,低版本得靠 JOIN 拼接。
例如查三级路径(根 → 二级 → 当前):
立即学习“PHP免费学习笔记(深入)”;
SELECT c1.name AS level1_name, c1.id AS level1_id, c2.name AS level2_name, c2.id AS level2_id, c3.name AS level3_name, c3.id AS level3_id FROM category c3 LEFT JOIN category c2 ON c3.parent_id = c2.id LEFT JOIN category c1 ON c2.parent_id = c1.id WHERE c3.id = ?
- 每多一级就得加一个
LEFT JOIN,5 级就要写 5 次 JOIN,维护成本高 - 必须用
LEFT JOIN,否则某级缺失会导致整行丢失(比如当前栏目是二级,c1 就为空) - 返回字段名带层级后缀(如
level2_name),PHP 中需手动过滤空值再组装数组
生成 HTML 面包屑时要注意链接和分隔符逻辑
直接 echo 字符串易出 XSS 和 URL 拼接错误。安全做法是先生成结构化数组,再统一渲染。
- 最后节点不加链接,用
包裹,避免用户点击刷新当前页 - 分隔符(如
>或/)不要硬编码在循环里,应在每项之间插入,即「第 i 项后加分隔符,i - URL 中的栏目 ID 必须用
intval()过滤,名称要用htmlspecialchars()转义 - 如果栏目 URL 是伪静态(如
/php/array),需额外维护slug字段,不能仅靠 ID 拼接
缓存路径数据能显著降低数据库压力
栏目结构变动频率远低于访问频率,全量缓存「每个栏目 ID 对应的面包屑数组」性价比极高。
- 推荐用 Redis 缓存,键名格式:
breadcrumb:{$cat_id},过期时间设为 1 小时足够 - 栏目增删改时,除了更新自身记录,还要删掉所有以它为祖先的缓存(如删了 ID=5 的栏目,要删
breadcrumb:5、breadcrumb:12、breadcrumb:27…) - 若不用 Redis,可用 APCu(PHP 内存缓存),但仅限单机部署,且需注意进程间不共享
- 切勿缓存整个 HTML 字符串——一旦站点主题变,缓存就失效;缓存结构化数组更灵活
路径拼接本身不难,难的是边界情况:ID 不存在、parent_id 循环引用、根节点 parent_id 值不统一(0 / NULL / -1)、多语言栏目下 name 字段来源不一致。这些地方不提前校验,上线后容易在某个角落突然崩掉面包屑。











