CodeIgniter 无限级分类易栈溢出,因递归查库导致调用栈过深;应改用单次查询+PHP引用组装树,避免N次SQL与环形引用,并统一处理NULL、中文编码及API响应。

CodeIgniter 中递归查询无限级分类为什么容易栈溢出
直接在模型里写 get_category_tree() 递归查数据库,不加深度限制或缓存,查到 10 层以上就可能报 Fatal error: Maximum function nesting level of '256' reached。CodeIgniter 默认没开 Xdebug 的嵌套限制,但 PHP 自身的 xdebug.max_nesting_level(默认 256)或 zend_extension 环境下实际调用栈很容易触顶。
真正该做的不是调高限制,而是换思路:用单次查询 + PHP 递归组装,而非 SQL 层递归(MySQL 8.0+ 虽支持 CTE,但 CI3 不原生适配,CI4 又需手动写原生语句)。
- 查全部分类时用
$this->db->order_by('parent_id, sort')->get('category')->result(),确保父节点一定在子节点前 - 避免在循环中反复调用
$this->db->where('parent_id', $id)->get()—— 每次都是新查询,N 层就是 N 次 SQL,还带 N² 级联开销 - 如果数据量超 500 条,建议加
cache_on或用 Redis 缓存整棵树,而不是每次请求都重算
把扁平数组转成树形结构的 PHP 实现要点
拿到 result() 返回的扁平数组后,不能靠简单 foreach 嵌套生成多维数组 —— 那样无法处理跨层级引用(比如 id=5 的 parent_id=2,但 id=2 还没被遍历到)。必须先建索引映射,再按 parent_id 归位。
核心是两步:建 $map(以 id 为 key 的引用数组),再遍历一次做挂载。示例关键逻辑:
$map = [];
foreach ($list as $item) {
$map[$item->id] = &$item;
}
$tree = [];
foreach ($list as $item) {
if ($item->parent_id == 0) {
$tree[] = &$item;
} else {
if (isset($map[$item->parent_id])) {
$map[$item->parent_id]->children[] = &$item;
}
}
}
- 必须用
&$item引用赋值,否则$map存的是副本,后续挂载无效 -
children字段名要统一,JSON 化时才不会有的有、有的空;建议在模型里初始化:$item->children = []; - 若原始数据含
NULLparent_id,需提前转成0,否则== 0判断失效
CodeIgniter 输出 JSON 时中文乱码和 null 字段问题
直接 json_encode($tree) 会把中文变 Unicode(如 \u4f60\u597d),且 PHP null 值在 JSON 里变成 null,前端解析易出错(尤其 Vue/React 依赖字段存在性)。
- 加
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES参数:json_encode($tree, JSON_UNESCAPED_UNICODE) - 对空数组/空对象补默认值:在构建树前,统一处理字段,例如
$item->name = (string)$item->name;,避免null冒出 - CI3 推荐用
$this->output->set_content_type('application/json')->set_output(...),比echo更安全(自动设 header,防 MIME 错误)
CI4 下如何复用这套逻辑又不破坏分页或 API 规范
CI4 的 Entity 和 Cast 机制让字段处理更干净,但递归树仍是手动活。关键是别把树形逻辑塞进 CategoryModel::findAll() —— 它该只负责查,组装交给 Service 层。
- 新建
CategoryService类,在buildTree()方法里做 map + 挂载,和控制器解耦 - 若需分页展示“某一级”的子类(如只查二级菜单),仍用
where('parent_id', $pid)单查,别硬套树形方法 - API 返回时,用
response()->setJSON($tree),它内部已处理字符集和 header,比手写json_encode更稳
树形结构的坑不在递归本身,而在数据一致性 —— 比如 parent_id 指向不存在的 id、环形引用(A 的 parent_id 是 B,B 的 parent_id 是 A)、或者事务中部分插入失败导致中间断层。上线前务必用 SELECT * FROM category WHERE parent_id NOT IN (SELECT id FROM category) AND parent_id != 0 扫一遍脏数据。


















