直接遍历+引用赋值是最稳妥做法:先用id建引用映射$map[$id] =& $item,再按parent_id归位子节点,最后收集parent_id为0或null的根节点。

PHP 中用 id 和 parent_id 平铺数据生成树形结构
直接遍历 + 引用赋值是最稳妥、性能可预期的做法,不依赖递归,避免栈溢出或重复查找。关键在于先建索引,再按 parent_id 归位。
常见错误是边查边递归:getChildren($id) 每次都全表扫描,1000 条数据可能触发上万次数组遍历;或者用 array_filter() 做嵌套筛选,时间复杂度直接升到 O(n²)。
实操建议:
- 第一步:遍历原始数组,用
id作键存一份引用映射 ——$map[$item['id']] = &$item; - 第二步:再遍历一次,对每个非根节点(
parent_id != 0且存在),把当前项追加进$map[$item['parent_id']]['children'][] - 第三步:收集所有
parent_id == 0(或 null)的项,它们就是顶层菜单 - 确保原始数据中
children字段未被预定义,否则引用赋值会出错;可统一用unset($item['children'])初始化
为什么不能直接用 array_reduce() 或 usort() 构建树
因为树形结构本质是父子引用关系,不是排序或聚合问题。usort() 只改变顺序,不建立嵌套;array_reduce() 在 PHP 中难以安全维护多层引用,容易丢失子节点或产生循环引用。
立即学习“PHP免费学习笔记(深入)”;
典型翻车场景:
- 用
array_reduce()累积时没做&$carry引用声明,导致子节点写不进父级children - 把
parent_id当成字符串比较(如'0'vs0),造成根节点识别失败 - 忽略空
parent_id是null还是'',导致isset($map[$item['parent_id']])判定为 false
处理带深度限制或路径字段的菜单(如 path 或 level)
如果原始数据已含 path(如 '0-5-12')或 level,说明它来自数据库的闭包表或预计算结果。这时不该再按 parent_id 重建树,而应按路径切分后逐层挂载。
实操要点:
- 先按
path字符串长度或level升序排序,保证父节点一定在子节点前出现 - 用一个临时引用栈:
$stack[$level] = &$currentNode;,每次根据当前level找到父级,再把当前节点塞进其children - 注意
path分隔符是否统一('-'还是'/'),避免explode()后首尾空字符串干扰层级判断 - 这种方案适合菜单项固定、读多写少的场景;但若频繁增删,维护
path成本远高于维护parent_id
输出前要检查的三个易漏点
生成完树结构,别急着 json_encode() 或渲染,先确认:
-
children是否为空数组而非null或未定义 —— 模板里foreach($item['children'] as ...)会报Invalid argument supplied for foreach() - 是否存在「孤儿节点」:即
parent_id对应的父节点不在原始数据中,又没被过滤掉,会导致$map[$pid]为undefined,后续引用赋值静默失败 - 是否混用了不同 ID 类型:比如
id是整型,parent_id是字符串,isset($map[$item['parent_id']])会返回 false(PHP 严格类型匹配)
最保险的方式是在第二步遍历时加一层存在性判断:if (isset($map[$item['parent_id']])) { ... },并把孤儿节点单独收集供排查。



















