codeigniter动态菜单核心是权限对齐而非单纯树形渲染:菜单项是否显示取决于用户是否拥有对应permission_code权限,需在模型层通过inner join权限表查询已授权且is_hidden=0的菜单,并按sort_order排序,禁止n+1查询与无限递归,缓存须绑定user_id并支持主动失效。

CodeIgniter 实现动态菜单配置,核心不是“怎么渲染一个树”,而是「如何让菜单结构与用户权限真正对齐」——菜单项该不该显示,取决于当前用户是否有对应 menu_id 或 permission_code 的访问权限,而不是单纯查出所有菜单再前端硬过滤。
菜单数据必须带权限标识字段
很多项目把菜单表设计成纯父子结构(id、parent_id、name、url),结果权限一加就崩:管理员能看到全部,普通用户点开空菜单或 403 报错。
真正可用的菜单表至少要包含:
-
permission_code(如user:list、order:export) -
is_hidden(布尔值,控制是否参与前端渲染,避免“有权限但不希望暴露入口”) -
sort_order(整型,避免靠 PHPusort动态排序,数据库 ORDER BY 更稳)
不要在控制器里用 get_all_menus() 拿全量菜单再 PHP 过滤——权限检查应下推到模型层,由 get_user_menus($user_id) 直接返回已授权且可展示的扁平/树形结构。
get_user_menus() 必须关联权限表做 INNER JOIN
常见错误是先查菜单,再 foreach 去查权限,N+1 查询直接拖慢首屏。正确做法是在模型中一条 SQL 完成:
SELECT m.id, m.parent_id, m.name, m.url, m.icon, m.sort_order FROM menus m INNER JOIN role_permissions rp ON m.permission_code = rp.permission_code INNER JOIN user_roles ur ON rp.role_id = ur.role_id WHERE ur.user_id = ? AND m.is_hidden = 0 ORDER BY m.sort_order
注意:INNER JOIN 保证只返回用户真实有权访问的菜单;is_hidden = 0 避免配置开关失效;ORDER BY 确保顺序可控,不依赖 PHP 排序。
- 如果角色支持多对多(用户可属多个角色),
ur.user_id = ?要配合DISTINCT去重 - URL 字段建议存相对路径(如
/admin/user/list),避免硬编码域名或协议 - 前端 JS 渲染时,用
url值做location.href跳转前,务必用site_url()包裹,否则可能跨域或 404
递归构树必须限制深度,且禁止无限嵌套渲染
build_menu_tree() 类函数若没设最大层级(比如超过 4 层就截断),容易因脏数据或逻辑漏洞导致 PHP 堆栈溢出或页面卡死。
推荐做法是:数据库查出扁平结果后,在 PHP 中用循环而非递归构建树,并设硬性深度限制:
$menus = $this->menu_model->get_user_menus($user_id); // 已按 sort_order 排好
$tree = [];
$stack = [0 => &$tree]; // parent_id => 引用
<p>foreach ($menus as $m) {
if ($m['depth'] > 4) continue; // 深度防护
if (!isset($stack[$m['parent_id']])) continue;
$item = [
'id' => $m['id'],
'name' => $m['name'],
'url' => $m['url'],
'children' => []
];
$stack[$m['parent_id']][] = &$item;
$stack[$m['id']] = &$item['children'];
}</p>
- 前端模板里渲染子菜单时,必须判断
!empty($item['children'])再展开<ul></ul>,否则 DOM 中会残留空列表 - Active 状态不能只靠 URL 匹配(
strpos($_SERVER['REQUEST_URI'], $menu['url']) !== false),要匹配完整路由段,否则/admin/user会误亮/admin/user/list的父项
缓存菜单结构但不可缓存权限判定结果
菜单结构本身变化频率低,可以用 $this->cache->save('menu<em>tree</em>'.$user_id, $tree, 3600) 缓存 1 小时;但权限判定(比如管理员临时禁用某人某个菜单)必须实时。
所以缓存键必须含 $user_id,不能用通用键如 all_menus;更不能把权限检查逻辑也塞进缓存数据里——那是把「状态」和「结构」混在一起,后期无法热更新。
最容易被忽略的一点:当用户角色变更(如从编辑升为管理员)后,旧菜单缓存未失效,会导致新权限不生效。解决方案只有两个:要么缩短缓存时间(如 10 分钟),要么在角色变更时主动 $this->cache->delete('menu_tree_'.$user_id)。











