当前位置:

首页 > 编程开发 > PHP递归实现无限级分类:将扁平数组转为树形结构

PHP递归实现无限级分类:将扁平数组转为树形结构

讲解如何使用PHP递归实现无限级分类,将带有id和parent_id的扁平分类数组转换为children嵌套的树形结构,并通过完整代码、执行结果、递归流程和常见问题帮助理解实现原理。

无限级分类的核心并不复杂:每条分类记录都有自己的 id,同时用 parent_id 指向父分类。PHP只需要根据这个父子关系不断寻找下一层节点,再通过递归把它们放进父节点的 children 中,就能把普通的一维数组转换成层层嵌套的树形结构。

下面从一组实际的分类数据开始,完整实现这个转换过程,并说明递归函数为什么能够处理任意层级。

一、先看扁平数组和树形结构有什么区别

数据库中的分类通常不会直接保存成树,而是一行一条记录。例如商品分类表可能得到下面的数据:

$categories = [
    ['id' => 1, 'name' => '数码产品', 'parent_id' => 0],
    ['id' => 2, 'name' => '电脑办公', 'parent_id' => 0],
    ['id' => 3, 'name' => '手机',     'parent_id' => 1],
    ['id' => 4, 'name' => '相机',     'parent_id' => 1],
    ['id' => 5, 'name' => '智能手机', 'parent_id' => 3],
    ['id' => 6, 'name' => '老人手机', 'parent_id' => 3],
    ['id' => 7, 'name' => '笔记本',   'parent_id' => 2],
    ['id' => 8, 'name' => '游戏本',   'parent_id' => 7],
];

这里约定 parent_id = 0 表示顶级分类。其他记录的 parent_id 等于某条记录的 id,就表示它属于那个分类。

例如:

  • 手机 的 parent_id 是 1,所以它属于“数码产品”。
  • 智能手机 的 parent_id 是 3,所以它属于“手机”。
  • 游戏本 的 parent_id 是 7,所以它属于“笔记本”。
PHP递归实现无限级分类:将扁平数组转为树形结构
扁平数组只保存父节点编号,树形结构则把这种关系转换为嵌套的 children。

我们最终希望得到的结构类似下面这样:

[
    [
        'id' => 1,
        'name' => '数码产品',
        'parent_id' => 0,
        'children' => [
            [
                'id' => 3,
                'name' => '手机',
                'parent_id' => 1,
                'children' => [
                    ['id' => 5, 'name' => '智能手机', 'parent_id' => 3],
                    ['id' => 6, 'name' => '老人手机', 'parent_id' => 3]
                ]
            ],
            [
                'id' => 4,
                'name' => '相机',
                'parent_id' => 1
            ]
        ]
    ]
]

这种结构特别适合生成多级菜单、商品分类、文章栏目、权限菜单以及地区层级等数据。

二、最直接的递归实现

理解无限级分类时,可以先使用最直观的写法:给递归函数传入一个父级ID,在整个数组中寻找 parent_id 等于这个ID的节点。找到节点以后,再以当前节点的 id 继续寻找它的子节点。

 1, 'name' => '数码产品', 'parent_id' => 0],
    ['id' => 2, 'name' => '电脑办公', 'parent_id' => 0],
    ['id' => 3, 'name' => '手机',     'parent_id' => 1],
    ['id' => 4, 'name' => '相机',     'parent_id' => 1],
    ['id' => 5, 'name' => '智能手机', 'parent_id' => 3],
    ['id' => 6, 'name' => '老人手机', 'parent_id' => 3],
    ['id' => 7, 'name' => '笔记本',   'parent_id' => 2],
    ['id' => 8, 'name' => '游戏本',   'parent_id' => 7],
];

function buildTree(array $items, int $parentId = 0): array
{
    $tree = [];

    foreach ($items as $item) {
        if ($item['parent_id'] == $parentId) {

            $children = buildTree($items, $item['id']);

            if (!empty($children)) {
                $item['children'] = $children;
            }

            $tree[] = $item;
        }
    }

    return $tree;
}

$tree = buildTree($categories);

print_r($tree);

真正起作用的是这一行:

$children = buildTree($items, $item['id']);

假设当前处理的是“数码产品”,它的 id 是 1,函数就会再次执行:

buildTree($items, 1);

这次只查找 parent_id = 1 的数据,因此可以找到“手机”和“相机”。处理“手机”时,又会执行:

buildTree($items, 3);

于是继续找到“智能手机”和“老人手机”。如果某个节点下面已经没有数据,递归函数返回空数组,这一条路径就结束。

PHP递归实现无限级分类:将扁平数组转为树形结构
递归不需要预先知道分类有几层:有子节点就继续调用,没有子节点就返回。

三、完整运行结果

执行上面的PHP脚本后,可以得到类似下面的结果。为了便于阅读,这里使用数组结构展示。

示例输出:

Array
(
    [0] => Array
        (
            [id] => 1
            [name] => 数码产品
            [parent_id] => 0
            [children] => Array
                (
                    [0] => Array
                        (
                            [id] => 3
                            [name] => 手机
                            [parent_id] => 1
                            [children] => Array
                                (
                                    [0] => Array
                                        (
                                            [id] => 5
                                            [name] => 智能手机
                                            [parent_id] => 3
                                        )

                                    [1] => Array
                                        (
                                            [id] => 6
                                            [name] => 老人手机
                                            [parent_id] => 3
                                        )
                                )
                        )

                    [1] => Array
                        (
                            [id] => 4
                            [name] => 相机
                            [parent_id] => 1
                        )
                )
        )

    [1] => Array
        (
            [id] => 2
            [name] => 电脑办公
            [parent_id] => 0
            [children] => Array
                (
                    [0] => Array
                        (
                            [id] => 7
                            [name] => 笔记本
                            [parent_id] => 2
                            [children] => Array
                                (
                                    [0] => Array
                                        (
                                            [id] => 8
                                            [name] => 游戏本
                                            [parent_id] => 7
                                        )
                                )
                        )
                )
        )
)

这里已经看不到固定的“一级分类、二级分类、三级分类”处理代码。无论以后在“游戏本”下面再增加多少层,只要 id 与 parent_id 的关系正确,函数都可以继续向下处理。

四、为什么递归能够实现“无限级”

所谓“无限级”并不是程序真的可以无限递归,而是代码没有把层级写死。例如下面这种方式只能处理固定层数:

// 不推荐:层级被写死
foreach ($level1 as $item1) {
    foreach ($level2 as $item2) {
        foreach ($level3 as $item3) {
            // 只能继续手工增加下一层
        }
    }
}

递归的区别在于,同一个函数可以调用自己:

function buildTree(array $items, int $parentId = 0): array
{
    // ...

    $children = buildTree($items, $item['id']);

    // ...
}

函数并不关心自己正在处理第2层还是第20层,只关心一个问题:当前节点下面还有没有直接子节点。

PHP递归实现无限级分类:将扁平数组转为树形结构
递归不断把当前节点的 id 当作下一轮 parent_id,直到当前分支找不到更多子节点。

五、数据量较大时,不要每层都扫描整个数组

前面的实现非常适合理解递归,但它有一个明显的问题:每处理一个节点,都要重新执行一次 foreach ($items as $item),也就是反复扫描整个分类数组。

分类只有几十条或几百条时通常不明显,但数据很多时,更合理的方式是先按照 parent_id 建立索引。例如:

$grouped = [
    0 => [
        ['id' => 1, 'name' => '数码产品', 'parent_id' => 0],
        ['id' => 2, 'name' => '电脑办公', 'parent_id' => 0],
    ],
    1 => [
        ['id' => 3, 'name' => '手机', 'parent_id' => 1],
        ['id' => 4, 'name' => '相机', 'parent_id' => 1],
    ],
    3 => [
        ['id' => 5, 'name' => '智能手机', 'parent_id' => 3],
        ['id' => 6, 'name' => '老人手机', 'parent_id' => 3],
    ]
];

这样递归到 parent_id = 3 时,可以直接访问 $grouped[3],不必重新遍历所有分类。

推荐的完整实现

 1, 'name' => '数码产品', 'parent_id' => 0],
    ['id' => 2, 'name' => '电脑办公', 'parent_id' => 0],
    ['id' => 3, 'name' => '手机',     'parent_id' => 1],
    ['id' => 4, 'name' => '相机',     'parent_id' => 1],
    ['id' => 5, 'name' => '智能手机', 'parent_id' => 3],
    ['id' => 6, 'name' => '老人手机', 'parent_id' => 3],
    ['id' => 7, 'name' => '笔记本',   'parent_id' => 2],
    ['id' => 8, 'name' => '游戏本',   'parent_id' => 7],
];

/**
 * 根据 parent_id 对分类进行分组
 */
function groupByParentId(array $items): array
{
    $grouped = [];

    foreach ($items as $item) {
        $parentId = (int) $item['parent_id'];
        $grouped[$parentId][] = $item;
    }

    return $grouped;
}

/**
 * 根据分组后的数据递归构造分类树
 */
function buildTreeFromGroup(array $grouped, int $parentId = 0): array
{
    if (!isset($grouped[$parentId])) {
        return [];
    }

    $tree = [];

    foreach ($grouped[$parentId] as $item) {
        $children = buildTreeFromGroup($grouped, (int) $item['id']);

        if (!empty($children)) {
            $item['children'] = $children;
        }

        $tree[] = $item;
    }

    return $tree;
}

$grouped = groupByParentId($categories);
$tree = buildTreeFromGroup($grouped);

echo '
';
print_r($tree);
echo '
';

这个版本仍然使用递归,但寻找子节点时已经不再扫描完整数组。整个处理过程可以理解成两部分:

  1. 先遍历一次原数组,按照 parent_id 建立分组。
  2. 从 parent_id = 0 开始递归,只访问当前父节点对应的那组数据。

如果一个父节点不存在于 $grouped 中:

if (!isset($grouped[$parentId])) {
    return [];
}

就说明当前节点已经没有下一层,这同时也是递归结束的条件。

如果分类数据会频繁参与菜单渲染,而原始数据一次取出后需要反复构建树,建议优先使用这种“先按 parent_id 分组,再递归”的方式,避免递归过程中不断扫描同一批数据。

六、把树形数组转换成JSON

如果分类树需要提供给前端JavaScript、Vue、React或接口调用,可以直接通过 json_encode() 输出。

header('Content-Type: application/json; charset=utf-8');

echo json_encode(
    $tree,
    JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT
);

示例输出:

[
    {
        "id": 1,
        "name": "数码产品",
        "parent_id": 0,
        "children": [
            {
                "id": 3,
                "name": "手机",
                "parent_id": 1,
                "children": [
                    {
                        "id": 5,
                        "name": "智能手机",
                        "parent_id": 3
                    },
                    {
                        "id": 6,
                        "name": "老人手机",
                        "parent_id": 3
                    }
                ]
            },
            {
                "id": 4,
                "name": "相机",
                "parent_id": 1
            }
        ]
    }
]

JSON_UNESCAPED_UNICODE 用于避免中文变成Unicode转义形式,JSON_PRETTY_PRINT 只是为了让调试时的JSON更容易阅读,正式接口是否使用可以根据需要决定。

七、如果希望所有节点始终包含children

前面的代码只在存在子分类时添加 children。因此叶子节点会是:

[
    'id' => 5,
    'name' => '智能手机',
    'parent_id' => 3
]

有些前端组件要求每一个节点都存在 children 字段,即使没有子节点也必须是空数组。这时可以直接修改递归函数:

function buildTreeFromGroup(array $grouped, int $parentId = 0): array
{
    if (!isset($grouped[$parentId])) {
        return [];
    }

    $tree = [];

    foreach ($grouped[$parentId] as $item) {
        $item['children'] = buildTreeFromGroup(
            $grouped,
            (int) $item['id']
        );

        $tree[] = $item;
    }

    return $tree;
}

叶子节点就会变成:

{
    "id": 5,
    "name": "智能手机",
    "parent_id": 3,
    "children": []
}

这两种形式没有绝对的优劣,关键看后续使用方需要哪一种数据格式。

八、实际使用时容易出现的几个问题

1. 顶级分类的parent_id不是0

示例使用 parent_id = 0 表示根节点,但实际项目也可能使用 null、-1 或其他值。递归函数的起始参数必须和数据库设计保持一致。

例如顶级分类使用 -1:

$tree = buildTreeFromGroup($grouped, -1);

2. parent_id指向不存在的分类

假设存在下面的数据:

[
    'id' => 20,
    'name' => '异常分类',
    'parent_id' => 999
]

但数组中根本没有 id = 999 的节点,那么从根节点0开始递归时,这条记录不会进入正常分类树。

这通常不是递归函数的问题,而是原始数据的父子关系已经不完整。需要检查数据库中的删除逻辑,避免删除父分类之后留下孤立的子分类。

3. 分类关系不能形成循环

如果A的父节点是B,而B的父节点又指向A,就形成了循环关系。更明显的错误是节点把自己设置成父节点:

[
    'id' => 10,
    'name' => '错误分类',
    'parent_id' => 10
]

这种数据会破坏树结构的基本前提,并可能导致递归无法正常结束。因此新增或修改分类时,应禁止把自身以及自身的后代节点设置成自己的父节点。

4. PHP递归并不是物理意义上的无限

“无限级分类”表示业务代码没有固定限制2级、3级或5级,并不代表可以构造真正无限深的数据。如果分类嵌套异常深,会增加函数调用和内存消耗。正常的网站栏目、商品分类、权限菜单通常不会设计成极端深度,但仍应避免错误数据导致异常递归。

PHP递归实现无限级分类:将扁平数组转为树形结构
无限级分类的实现重点不是层数,而是维护正确的父子关系,并让递归在没有子节点时自然结束。

九、封装成项目中可以直接使用的函数

如果只需要一个可以直接放入项目的版本,可以把分组和递归构造封装在同一个入口函数中,对外只接收原始数组和根节点ID。

function arrayToTree(array $items, int $rootParentId = 0): array
{
    $grouped = [];

    foreach ($items as $item) {
        $parentId = (int) $item['parent_id'];
        $grouped[$parentId][] = $item;
    }

    $build = function (int $parentId) use (&$build, &$grouped): array {
        if (!isset($grouped[$parentId])) {
            return [];
        }

        $result = [];

        foreach ($grouped[$parentId] as $item) {
            $children = $build((int) $item['id']);

            if ($children !== []) {
                $item['children'] = $children;
            }

            $result[] = $item;
        }

        return $result;
    };

    return $build($rootParentId);
}

调用时只需要:

$tree = arrayToTree($categories);

如果项目中的根分类使用 -1:

$tree = arrayToTree($categories, -1);

这个函数完成的工作可以概括为:

  1. 读取扁平数组中的 id 和 parent_id。
  2. 按照 parent_id 对节点进行分组。
  3. 从根节点开始寻找直接子节点。
  4. 对子节点再次执行相同逻辑。
  5. 把返回的数据写入当前节点的 children。
  6. 某个节点找不到子节点时结束这一条递归分支。
总结: PHP实现无限级分类并不需要为一级、二级、三级分别编写处理代码。只要扁平数组中保存了可靠的 id 和 parent_id,就可以从根节点开始递归查找子分类,再把结果写入 children。小规模数据可以直接递归扫描数组;数据量较大时,先按 parent_id 建立索引再递归更合适。实际项目还应注意孤立节点、自引用和循环父子关系,否则问题往往来自原始分类数据,而不是递归代码本身。
本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
作者最新文章
编程开发
相关文章 更多
PHP递归遍历目录文件:RecursiveDirectoryIterator与自定义实现
PHP递归遍历目录文件:RecursiveDirectoryIterator与自定义实现

深入解析在PHP中递归遍历多级目录文件的两种核心方案:基于SPL标准库的RecursiveDirectoryIterator高效流式迭代,与基于scandir/opendir的自定义递归函数实现,剖析执行机制、内存表现与实战避坑指南。

PHP递归中static变量与引用传递的常见陷阱及调试
PHP递归中static变量与引用传递的常见陷阱及调试

本文分析PHP递归中static变量导致的状态污染及引用传递引发的共享数据修改问题。提供具体的代码复现、缓存键设计建议及调试打印技巧,帮助开发者避免隐蔽的逻辑错误。

查看更多
精品专题 更多
装机必备
装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows
Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

PDF教程
PDF教程

正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

Mac软件 更多
AirBuddy 3 macOS版
AirBuddy 3 macOS版
macOS

AirBuddy 3 是面向 macOS 的蓝牙外设增强工具,可集中查看 AirPods、Beats 与苹果设备电量,提供快速连接、播放控制、电量提醒、桌面小组件和多台 Mac 外设接力等功能。

AirServer macOS版
AirServer macOS版
macOS

AirServer macOS版可把 Mac 转换为无线投屏接收端,通过 AirPlay 或 Google Cast 接收手机、平板和电脑画面,并支持多设备同时连接、高清镜像及会话录制,适合家庭娱乐、课堂教学、会议演示和应用展示。

AnyDesk macOS版
AnyDesk macOS版
macOS

AnyDesk macOS版是一款适用于远程办公、设备维护和客户支持的远程桌面工具,可连接并操作异地计算机,支持文件传输、无人值守访问、会话权限控制及跨平台协作。

WINDOWS 更多
讯飞绘文 网页版
讯飞绘文 网页版
Windows

讯飞绘文网页版是科大讯飞推出的AI图文创作与内容运营工具,覆盖热点选题、文章生成、稿件校对、智能配图、排版发布和数据复盘等环节,适合自媒体创作者及内容团队在线使用。

AirServer Windows版
AirServer Windows版
Windows

AirServer Windows版可把电脑变成AirPlay、Google Cast和Miracast无线投屏接收端,适合会议演示、课堂教学及家庭大屏分享。发送设备通常无需另装客户端,并可同时接收和智能排列多个镜像会话。

AlwaysUp Windows版
AlwaysUp Windows版
Windows

AlwaysUp是一款面向Windows的系统服务注册与程序托管工具,可让普通应用、脚本及批处理在无人登录时随系统启动,并通过状态检测、异常重启、资源监控和邮件通知保障关键任务持续运行。