Yii框架实现无限级分类推荐一次性查全表+递归构建树结构,避免N+1查询;用ArrayHelper::map()配合缩进字段渲染下拉框层级。

直接说结论:Yii 框架实现无限级分类,**推荐一次性查全表 + 递归构建树结构**,而不是每次递归都查一次数据库;用 ArrayHelper::map() 配合带缩进的 new_cat_name 字段,就能在下拉框里渲染出清晰层级。
为什么不能在递归里反复查数据库
常见错误是写一个 getTree($pid) 方法,在循环中对每个子节点再调用一次查询 —— 这会导致 N+1 查询问题。比如 10 个一级分类、每个下面平均 5 个二级、再各带 3 个三级,最终可能触发 1 + 10 + 50 + 150 = 211 次 SQL 查询。实际项目中数据稍多就卡顿甚至超时。
- 必须先用
Category::find()->asArray()->all()一次性取全量数据 - 所有后续逻辑都在 PHP 数组内完成,不碰 DB
- 如果真要用嵌套集合(Nested Sets),得引入
creocoder/yii2-nested-sets扩展,但那是另一套建模方式,和本文的 parentId 方案不混用
怎么写安全又可读的递归构建函数
核心是避免全局变量、防止无限递归、支持任意深度。以下是最简可用版本:
protected function buildTree($items, $parentId = 0, $level = 1)
{
$tree = [];
foreach ($items as $item) {
if ($item['parent_id'] == $parentId) {
$item['level'] = $level;
$item['indent_name'] = str_repeat('— ', $level - 1) . $item['cat_name'];
$item['children'] = $this->buildTree($items, $item['id'], $level + 1);
$tree[] = $item;
}
}
return $tree;
}-
$level从 1 开始,避免str_repeat('— ', 0)出空格错位 - 不修改原始
$items数组,返回新结构,方便调试和复用 - 字段名统一用
parent_id和id,别混用pid/cateid等别名,否则ArrayHelper::map()映射会失败
下拉框渲染时怎么让父子关系一目了然
关键不是靠前端 JS 拼接,而是后端提前把缩进字符串算好,传给视图。常见坑是:str_repeat('--', $lev) 里用了两个短横,但实际显示时字体等宽性差,建议改用全角破折号或中文空格更稳。
- 控制器里调用:
$tree = $this->buildTree($categories); $flat = $this->flattenTree($tree); - 加一个扁平化辅助方法(用于下拉):
protected function flattenTree($tree, &$result = []) { foreach ($tree as $node) { $result[] = ['id' => $node['id'], 'name' => $node['indent_name']]; if (!empty($node['children'])) { $this->flattenTree($node['children'], $result); } } return $result; } - 视图中直接用:
= $form->field($model, 'parent_id')->dropDownList(ArrayHelper::map($flat, 'id', 'name'), ['prompt' => '请选择父级']) ?>
容易被忽略的边界点
真实业务里最常翻车的不是逻辑,而是数据本身:顶级分类的 parent_id 值到底是 0、null 还是 ''?数据库字段类型是 INT 还是 SMALLINT?这些细节一旦不一致,== 就会静默失败。
- 查库前强制统一类型:
Category::find()->asArray()->where(['parent_id' => (int)$pid])->all() - 递归入口处补默认值:
if ($parentId === null) { $parentId = 0; } - 数据库索引必须有:
KEY `parent_id` (`parent_id`),否则万级数据下构建树要秒变分钟级


















