在PHP后台开发里,我们经常会从数据库拿到类似分类表那样的数据:每一行有自己的标识和上级标识,整体是一个二维数组。如果想把它压成一维数组,并且键名能体现从根到叶子的层级路径,就需要做键名拼接和引用写入。这种做法在生成YAML前的中间结构、或者前端不需要嵌套只要扁平带路径的配置时特别有用。

理解二维转一维树形键名的核心原理
所谓二维转一维树形键名,本质是把原本 array(array('id'=>1,'name'=>'a','pid'=>0), ...) 这样的结构,变成 array('a'=>..., 'a/b'=>..., 'a/b/c'=>...) 的形式。这里的斜杠不是必须,你可以换成下划线或者点号,重点是键名中携带了祖先链信息。实现时不能简单用 array_merge,因为那样会丢掉层级;必须使用递归或栈来追踪当前路径。
最关键的技术点是PHP的引用符号 &。当我们找到某个节点的所有子节点,希望把它们挂到以当前节点路径为前缀的键下时,如果不用引用,就会复制数组导致性能浪费且改不动原目标。通过 $target =& $result[$path] 这类写法,可以把子树的写入直接落到结果数组的对应位置。下面这段示例展示了基础递归思路:
<?php
$data = array(
array('id'=>1, 'name'=>'root', 'pid'=>0),
array('id'=>2, 'name'=>'child1', 'pid'=>1),
array('id'=>3, 'name'=>'child2', 'pid'=>1),
array('id'=>4, 'name'=>'sub', 'pid'=>2),
);
function buildTree($data, $pid=0, $prefix='', &$result=array()) {
foreach ($data as $row) {
if ($row['pid'] == $pid) {
$path = $prefix === '' ? $row['name'] : $prefix . '/' . $row['name'];
$result[$path] = $row['id'];
buildTree($data, $row['id'], $path, $result);
}
}
return $result;
}
$out = buildTree($data);
print_r($out);
?>
上面的代码把每个节点的名称拼成路径,值存为自己的id。如果节点名称里本身含有斜杠,就要先替换掉,否则路径会错乱。另外当数据量很大时,递归深度可能触发 xdebug.max_nesting_level 限制,这时要改用迭代写法。
递归与迭代两种实现方式对比
递归版本代码直观,容易理解父子关系,但每进入一层就会新增函数栈。对于几千行的分类数据,递归完全没问题;但若达到几万层深,就可能内存溢出。迭代版本通常使用显式栈或队列来保存待处理节点和当前路径,能有效控制调用深度。下面给出迭代写法,用数组模拟栈:
<?php
function buildTreeIter($data) {
$result = array();
$stack = array(array('pid'=>0, 'prefix'=>''));
while (!empty($stack)) {
$cur = array_pop($stack);
foreach ($data as $row) {
if ($row['pid'] == $cur['pid']) {
$path = $cur['prefix'] === '' ? $row['name'] : $cur['prefix'] . '/' . $row['name'];
$result[$path] = $row['id'];
$stack[] = array('pid'=>$row['id'], 'prefix'=>$path);
}
}
}
return $result;
}
?>
从可维护性看,递归更贴近树的自然定义,新人容易接手;迭代则更适合写工具函数。性能上二者时间复杂度都是 O(n^2) 因为内层循环扫全表,若先按 pid 分组建索引可降到 O(n)。在键名冲突时,比如两个不同分支下都有同名节点,后写的会覆盖先写的,因此需要把id也编入键名,例如 $path . '#' . $row['id'] 来避险。
实际项目中,我们往往还要过滤掉停用节点。可以在循环里加状态判断,只有 status=1 才向下传递。这样生成的树形键名一维数组直接 json_encode 就能给前端做级联选择,不需要再解析嵌套。
处理特殊字符与重复键的落地建议
键名作为路径,最怕出现分隔符冲突。假设用户分类叫“男/女”,用斜杠拼接就会变成“root/男/女/child”,解析时无法区分是几级。通用做法是入库前把名称中的斜杠替换成全角或下划线,或者改用 json_encode 后的id链做键。下面的示例演示了键名安全处理函数:
<?php
function safeName($name, $sep='/') {
return str_replace($sep, '_', $name);
}
function buildSafe($data) {
$result = array();
$stack = array(array('pid'=>0, 'prefix'=>''));
while (!empty($stack)) {
$cur = array_pop($stack);
foreach ($data as $row) {
if ($row['pid'] == $cur['pid']) {
$nm = safeName($row['name']);
$path = $cur['prefix'] === '' ? $nm : $cur['prefix'] . '/' . $nm;
$result[$path] = $row['id'];
$stack[] = array('pid'=>$row['id'], 'prefix'=>$path);
}
}
}
return $result;
}
?>
重复键问题除了拼id,还可以用 isset 检测后追加序号。例如发现 $result[$path] 已存在,就写成 $path . '_2'。不过这样会让路径不统一,更推荐在业务层保证名称唯一,或者接受id后缀。最后提醒,PHP数组键会自动转成整数如果看起来像数字,所以纯数字路径要用引号包住或加前缀字母,避免 123 变成 123 整型键导致后续 array_keys 类型不对。
当你把这些一维树形键名结构写进文件缓存,比如用 var_export 生成 PHP 数组文件,注意转义单引号即可,不需要额外处理。整个转换过程不依赖任何扩展,原生PHP就能完成,是非常轻量的数据结构整理方案。