递归构建父子关系前必须确保数据结构支持嵌套:先一次性查出所有记录并用id建索引数组,按parent_id、id排序;递归函数只挂载子树、不查库;需校验parent_id合法性防环和爆栈;深度大时建议改用迭代。

递归前先确认数据结构是否支持嵌套
PHP 里用递归构建父子关系,前提是数据库查出来的数据本身得能表达层级。常见错误是直接 SELECT * FROM menu 拿到扁平结果就开递归,但没处理 parent_id 和 id 的映射关系,导致递归找不到父节点或死循环。
正确做法是:先用一次查询把所有记录加载进内存,再按 id 建索引数组,确保能在 O(1) 时间内定位任意节点的父引用:
$rows = $pdo->query("SELECT id, parent_id, name FROM menu ORDER BY parent_id, id")->fetchAll();
$map = [];
foreach ($rows as $row) {
$map[$row['id']] = $row;
}
- 必须用
ORDER BY parent_id, id,避免子节点在父节点前出现导致临时找不到父项 - 不要在递归函数里反复查库——那是 N+1 问题,不是递归问题
- 如果表有上万行,别硬扛,考虑改用闭包表或路径枚举
递归函数里只做“挂载”,不查库不拼 SQL
递归函数职责要单一:接收一个父 ID,找出所有直接子节点,逐个递归,并把子树挂到父节点的 children 键下。别在里面调 query(),也别拼 WHERE parent_id = ?。
function buildTree($parentId = 0, &$map) {
$tree = [];
foreach ($map as $node) {
if ($node['parent_id'] == $parentId) {
$node['children'] = buildTree($node['id'], $map);
$tree[] = $node;
}
}
return $tree;
}
- 传引用
&$map是为了复用同一份数据,避免重复拷贝 - 返回空数组
[]而不是null,下游遍历时不用额外判空 - 如果某节点
parent_id为NULL,记得统一转成0或对应根标识,否则匹配失败
防止无限递归的关键是检查 parent_id 是否合法
实际数据常有脏数据:比如 parent_id = 9999,但 ID 9999 不存在;或者 A → B → A 形成环。不做校验,递归会卡死或爆栈。
最简防御方式是在递归入口加一层存在性判断:
function buildTree($parentId = 0, &$map) {
// 父节点本身不存在?终止递归
if ($parentId !== 0 && !isset($map[$parentId])) {
return [];
}
// …后续逻辑不变
}
-
$parentId === 0是根节点约定,不检查是否存在 - 如果业务允许“虚拟父级”,需额外维护一个合法
parent_id白名单 - PHP 默认栈深度约 100 层,超过会报
Fatal error: Maximum function nesting level,不是代码错,是数据环或过深
用迭代替代递归更稳,尤其数据深度不确定时
递归写起来顺,但 PHP 没尾递归优化,深度超 50 层就容易出事。真实业务中菜单、组织架构往往嵌套很深,建议用栈模拟:
$stack = [0]; // 起始父ID
$result = [];
$lookup = []; // id → node 引用
<p>foreach ($rows as $row) {
$lookup[$row['id']] = ['data' => $row, 'children' => []];
}</p><p>while (!empty($stack)) {
$pid = array_pop($stack);
foreach ($rows as $row) {
if ($row['parent_id'] == $pid) {
$node = &$lookup[$row['id']];
if (isset($lookup[$pid])) {
$lookup[$pid]['children'][] = &$node;
}
$stack[] = $row['id'];
}
}
}</p>
- 迭代不依赖调用栈,深度无硬限制
- 注意用
&$node和&$lookup[$pid]保持引用,否则赋值的是副本 - 如果只需要输出 HTML 或 JSON,其实连完整树都不用建,边查边渲染更快
真正麻烦的从来不是怎么写递归,而是数据里藏着没暴露的环、缺失的父节点、类型不一致的 parent_id(比如字符串 '0' 和整数 0)。跑之前先 SELECT parent_id FROM menu WHERE parent_id NOT IN (SELECT id FROM menu) AND parent_id != 0 扫一遍。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











