递归遍历PHP树形数组需从顶层节点(parent_id为0或null)开始,用foreach配合自调用,避免死循环须加深度限制、环检测及根节点动态识别,且$node为副本故不可原地修改children。

递归遍历 PHP 树形数组的核心写法
直接用 foreach 配合函数自调用就能搞定,关键不是“能不能”,而是“怎么避免死循环和键名错位”。树形数组通常以 id 和 parent_id(或 pid)建模,子节点藏在父节点的 children 键下,递归入口得从顶层节点(parent_id === 0 或 null)开始。
最简可行版本:
function traverseTree($nodes, $parentId = 0) {
$result = [];
foreach ($nodes as $node) {
if ($node['parent_id'] == $parentId) {
$node['children'] = traverseTree($nodes, $node['id']);
$result[] = $node;
}
}
return $result;
}
- 必须显式传入
$parentId初始值(常见是0或null),不能硬编码在函数内 - 每次递归都全量扫描
$nodes,适合几百条以内数据;上万条建议先预构建索引数组 - 注意比较运算符:用
==可能触发类型转换(如'0' == 0成立),生产环境推荐===并统一字段类型
为什么 foreach 里修改原数组的 children 键会失效
因为 PHP 数组是值传递(即使传的是变量名),foreach ($nodes as $node) 中的 $node 是副本,直接改 $node['children'] = [...] 不会影响原始 $nodes 数组。你看到的“没生效”,其实是这个原因。
- 正确做法:把子树结果赋给新结构(如上面示例返回
$result),而不是试图就地修改输入参数 - 若坚持要原地构建,需用引用:
foreach ($nodes as &$node),但要注意后续循环中$node引用未释放会导致意外覆盖 - 更安全的替代:用
array_map+ 闭包,但可读性下降,小项目不必要
用 SPL RecursiveIterator 实现非递归式遍历
当树深度不确定、担心栈溢出(比如嵌套超 100 层),或需要统一处理(如导出为 JSON、计算层级深度),SPL 的 RecursiveArrayIterator 更稳。
立即学习“PHP免费学习笔记(深入)”;
$it = new RecursiveIteratorIterator(
new RecursiveArrayIterator($tree),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($it as $key => $value) {
$depth = $it->getDepth();
// $key 是当前键名(如 'name', 'id'),不是节点本身
// 需结合 $it->current() 和 $it->getSubIterator() 提取完整节点
}
-
RecursiveArrayIterator只认array和Traversable,对关联键无特殊处理,children必须是数组,不能是对象 -
getDepth()返回当前嵌套层级,从 0 开始,比手算$level++更可靠 - 性能略低于纯
foreach递归,但内存占用更可控,适合 CLI 脚本长时间运行场景
容易被忽略的边界情况
真实业务里,树经常不“标准”:有环引用、缺失根节点、parent_id 指向不存在的 id、空数组输入……这些不提前兜底,上线后就是 Fatal error: Maximum function nesting level of '256' reached。
- 加深度限制:在递归函数开头判断
if ($depth > 20) { throw new Exception('Tree too deep'); } - 检查环:维护一个已访问
id的数组,每次递归前in_array($node['id'], $visited),命中则报错 - 根节点识别逻辑别写死:用
array_filter($nodes, fn($n) => !isset($n['parent_id']) || $n['parent_id'] == 0)替代硬编码parent_id === 0 - 空输入直接返回空数组,不要让
foreach在null上报 Warning
递归本身不难,难的是树数据从来不是理想模型。多一重校验,少一次线上排查。



















