直接递归易爆栈或查库过载,因PHP默认限制对深度嵌套+每层查库极敏感;应一次性查全数据后内存建树,用数组模拟Composite模式,配合环检测与深度限制确保安全。

为什么直接递归容易爆栈或查库过载
PHP 默认的 max_execution_time 和内存限制对深度嵌套(比如 >10 层)+ 每层查一次数据库的递归非常敏感。常见错误是写一个 getChildren($id) 函数,在循环里反复调用自己,同时每次调用都执行 SQL —— 这会触发 N+1 查询,10 层嵌套可能发起上百次查询,还容易卡在 Fatal error: Maximum function nesting level。
真正该做的是「一次性查全、内存中建树」:先用单次查询取出全部分类(带 id、parent_id、name 等字段),再用 PHP 数组组织层级关系。
- 查全语句示例:
SELECT id, parent_id, name FROM category ORDER BY parent_id, sort_order - 确保
parent_id为 NULL 或 0 表示顶级,且数据无环(否则递归永远出不来) - 不要在递归函数里做 DB 查询,所有数据必须提前加载进数组
用 PHP 数组模拟 Composite 模式,不依赖类继承
Composite 模式本质是让「单个节点」和「节点集合」对外提供统一接口(如 render()、getDepth())。但在 PHP 中,你不需要硬套 Component / Leaf / Composite 类结构——用关联数组 + 递归闭包更轻量、更易调试。
关键点在于:每个节点数组包含 children 键,其值是子节点数组(可能为空),这样就能自然支持无限深遍历。
立即学习“PHP免费学习笔记(深入)”;
- 构建树的函数只需一次遍历原始数据:
buildTree($categories, $parentId = null) - 返回结构形如:
['id'=>1, 'name'=>'手机', 'children'=>[...]],子项同构 - 渲染时用递归闭包:
$render = function($node, $level = 0) use (&$render) { ... };,避免类定义开销
如何安全控制递归深度并防止死循环
即使数据已查全,若存在脏数据(例如 A→B→C→A 的环引用),纯递归仍会无限下去。不能只靠 set_error_handler 捕获致命错误,得主动设防。
- 在构建树前加环检测:用
array_key_exists($id, $visited)记录路径,发现重复 ID 立即抛出异常 - 递归渲染时传入最大深度参数:
$render($node, $level + 1),并在入口判断if ($level > 12) throw new RuntimeException('Max depth exceeded') - MySQL 层面可在查询时加
WHERE level (如果表有 <code>level字段),但不如 PHP 层控制灵活
需要输出 HTML 列表?别拼接字符串,用生成器节省内存
当分类数达上千条,用字符串拼接生成嵌套 <ul><li> 容易吃光内存。改用 Generator 分块产出,配合 yield 流式处理。
示例核心逻辑:
function renderTree($tree, $level = 0) {
foreach ($tree as $node) {
yield str_repeat(" ", $level) . "<li>" . htmlspecialchars($node['name']);
if (!empty($node['children'])) {
yield str_repeat(" ", $level) . "<ul>";
yield from renderTree($node['children'], $level + 1);
yield str_repeat(" ", $level) . "</ul>";
}
yield str_repeat(" ", $level) . "</li>";
}
}
调用时:foreach (renderTree($tree) as $line) echo $line . "\n"; —— 每次只 hold 一层节点,深度不影响内存峰值。
注意:htmlspecialchars() 必须加,否则用户输入的 <script> 会直接执行;str_repeat() 仅用于缩进可读性,生产环境可删。



















