
本文介绍在构建树形 json 数据时,如何确保所有非叶子节点(即 `leaf => false`)均包含 `children => []` 字段,即使其实际无子项,从而满足前端组件(如 extjs、ant design tree)对标准树结构的严格要求。
在后端生成嵌套树形数据(如组织架构、菜单列表)时,常见需求是:所有 leaf => false 的节点必须显式声明 children 键,且值为空数组 [],而非完全省略该字段。否则,前端树组件可能因结构不一致而报错或渲染异常。
原始代码的问题在于:仅在发现子节点时才动态追加到父节点的 children 数组中,但从未为那些本应存在却暂无子项的父节点初始化 children => []。这导致部分父节点缺失 children 键,破坏了树结构的完整性。
✅ 正确做法:统一初始化 + 条件追加
关键改进点在于 两阶段处理:
- 第一阶段(初始化):遍历所有记录,为每个节点(无论是否被引用为父节点)预先设置 children => [];
- 第二阶段(关联):再遍历一次,将子节点正确挂载到对应父节点的 children 数组中。
以下是优化后的完整实现(含注释与健壮性增强):
$res = $dbConn->prepare($sql);
$res->execute();
$result = $res->fetchAll(PDO::FETCH_ASSOC);
// 步骤 1:建立 ID → 节点引用映射,并统一初始化 children 为空数组
$tmpData = [];
foreach ($result as &$val) {
// 强制初始化 children,确保每个节点都有该键
$val['children'] = [];
$tmpData[$val['id']] = &$val;
}
// 步骤 2:建立父子关系 —— 将子节点追加到父节点的 children 中
foreach ($result as &$val) {
$parentId = $val['groupid'] ?? null;
// 仅当存在有效 groupid 且该父节点存在于 tmpData 中时才挂载
if ($parentId && isset($tmpData[$parentId])) {
$tmpData[$parentId]['children'][] = &$val;
}
}
// 步骤 3:提取根节点(无 parent 的节点)
$roots = [];
foreach ($result as $val) {
if (empty($val['groupid']) || !isset($tmpData[$val['groupid']])) {
$roots[] = $val;
}
}
// 最终输出为标准树结构(JSON-ready)
echo json_encode($roots, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);⚠️ 注意事项与最佳实践
- 避免重复初始化:不要在 else 分支中初始化 children(如原答案中 $tmpData[$val['groupid']]['children'] = []),因为 $val['groupid'] 指向的是父节点 ID,而该父节点可能尚未在 $tmpData 中创建(例如父节点记录排在子节点之后),导致 undefined index 错误。
- &$val 引用安全:使用引用时需确保后续未意外修改原始 $result,建议在构建完树后及时释放引用(PHP 7+ 通常自动管理,但复杂逻辑中可显式 unset($val))。
- leaf 字段同步更新:若业务逻辑依赖 leaf 布尔值,应在挂载完成后自动修正——例如:$node['leaf'] = empty($node['children']);,确保语义准确。
- 性能提示:对于大数据集,可考虑使用 array_column($result, null, 'id') 替代手动循环构建 $tmpData,提升可读性与效率。
通过上述方法,你将获得符合规范的、零缺失字段的树形结构,既满足前端强约束,又保持 PHP 后端逻辑清晰可控。










