怎样在PHP中实现树形结构的菜单管理?应用组合模式Composite统一处理叶子节点

阿宇同学_9660

阿宇同学_9660

2026-04-28

779人浏览

原创

直接递归查数据库做菜单树易引发n+1查询和逻辑分散问题;组合模式通过menucomponent接口统一menuitem与menugroup行为,实现权限校验、渲染等逻辑内聚,避免模板分支判断与重复创建。

怎样在php中实现树形结构的菜单管理?应用组合模式composite统一处理叶子节点

为什么直接递归查数据库做菜单树容易出问题

因为每次展开子菜单都要重新查一次数据库,10级菜单可能触发上百次查询;更麻烦的是,当需要给所有节点统一加权限校验、URL重写或缓存标记时,逻辑会散落在各层递归里,改一处漏十处。

组合模式不是炫技——它把 MenuComponent 定义成统一接口,让 MenuItem(叶子)和 MenuGroup(容器)都实现它,后续增删节点、遍历渲染、权限过滤全走同一套调用路径,不用再判断「当前是数组还是对象」。

  • 避免在模板里写 if (isset($item->children)) 这类分支判断
  • 新增「快捷入口组」或「灰度菜单项」时,只需新增一个类,不改遍历逻辑
  • 序列化整个菜单树时,json_encode() 能自然处理嵌套结构,无需手动 flatten

Composite 接口和两个关键实现类怎么写

PHP 里不需要抽象基类,接口足够。重点是让叶子和容器对外行为一致:都能 render()、都能 hasPermission()、都能 getRoute()(叶子返回真实 URL,容器返回 # 或空字符串)。

interface MenuComponent
{
    public function render(): string;
    public function hasPermission(string $userRole): bool;
    public function getRoute(): string;
}

class MenuItem implements MenuComponent
{
    public function __construct(
        private string $label,
        private string $route,
        private array $roles = ['*']
    ) {}

    public function render(): string
    {
        return "
  • route}\">{$this->label}
  • "; } public function hasPermission(string $userRole): bool { return in_array('*', $this->roles) || in_array($userRole, $this->roles); } public function getRoute(): string { return $this->route; } } class MenuGroup implements MenuComponent { private array $children = []; public function __construct(private string $label) {} public function add(MenuComponent $child): void { $this->children[] = $child; } public function render(): string { $items = ''; foreach ($this->children as $child) { if ($child->hasPermission($_SESSION['role'] ?? 'guest')) { $items .= $child->render(); } } return "
  • {$this->label}
      {$items}
  • "; } public function hasPermission(string $userRole): bool { // 容器本身无权限,只看子项是否至少有一个可见 foreach ($this->children as $child) { if ($child->hasPermission($userRole)) { return true; } } return false; } public function getRoute(): string { return '#'; } }

    从数据库构建组合树时最容易踩的坑

    别在循环里 new 一堆 MenuGroup 再手动 add() —— 这样无法复用已存在的分组实例,导致相同父级被重复创建,权限判断错乱。

    btpanel phpsite 宝塔面板PHP网站
    btpanel phpsite 宝塔面板PHP网站

    宝塔面板 PHP 网站管理:站点创建、删除、启停、PHP 版本切换、域名管理、SSL证书管理、伪静态管理、数据库管理

    下载
    • 用递归查询 + 一次 fetch 所有数据,按 parent_id 分组后,用引用构建父子关系
    • 每个 MenuGroup 实例必须全局唯一,建议用 ID 做键存进 $groupsById = [],避免同名分组覆盖
    • 叶子节点的 $roles 字段必须从数据库读取,不能硬编码;若数据库没存角色字段,MenuItem 构造时默认传 ['*'],但要在文档里标清楚这是降级策略
    • 注意 MySQL 的 GROUP BY 和 PHP 数组 key 类型:整数 ID 当作字符串 key 用会导致找不到父节点

    渲染时如何避免 N+1 查询和无限递归

    组合模式本身不解决数据加载问题。如果你在 render() 里偷偷查数据库,那只是把 N+1 换了个地方发生。

    真正安全的做法是:在构建树之前,用一条 JOIN 或两次查询把全部菜单项 + 全部权限规则捞上来,全部塞进内存;MenuGroup::render() 和 MenuItem::render() 都只操作已有对象,不触发任何 I/O。

    • 测试时故意把某个 parent_id 指向不存在的 ID,观察是否抛出异常而不是静默跳过——这说明你的构建逻辑没做环路检测
    • 对深度超过 5 级的菜单,加个 $depth 参数控制递归层数,防止配置错误导致栈溢出
    • 如果菜单要支持多语言,render() 不该直接拼 HTML,而是返回结构化数组,交给视图层处理翻译

    组合模式的价值不在“看起来像一棵树”,而在于把「谁来决定这个节点要不要显示」这件事,从模板下沉到对象内部。一旦你开始在 hasPermission() 里混入日志、缓存、AB 测试分流逻辑,就会发现——当初少写的那几个 if 判断,省下的不是代码行数,是后续三个月的排查时间。

    php免费学习视频:立即使用
    踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!

    相关文章

    PHP速学教程(入门到精通)
    PHP速学教程(入门到精通)

    PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

    下载

    相关标签:

    php

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

    相关专题

    更多
    php文件怎么打开
    php文件怎么打开

    打开php文件步骤:1、选择文本编辑器;2、在选择的文本编辑器中,创建一个新的文件,并将其保存为.php文件;3、在创建的PHP文件中,编写PHP代码;4、要在本地计算机上运行PHP文件,需要设置一个服务器环境;5、安装服务器环境后,需要将PHP文件放入服务器目录中;6、一旦将PHP文件放入服务器目录中,就可以通过浏览器来运行它。

    2023.09.01

    9924

    6

    php怎么取出数组的前几个元素
    php怎么取出数组的前几个元素

    取出php数组的前几个元素的方法有使用array_slice()函数、使用array_splice()函数、使用循环遍历、使用array_slice()函数和array_values()函数等。本专题为大家提供php数组相关的文章、下载、课程内容,供大家免费下载体验。

    2023.10.11

    5921

    5

    php反序列化失败怎么办
    php反序列化失败怎么办

    php反序列化失败的解决办法检查序列化数据。检查类定义、检查错误日志、更新PHP版本和应用安全措施等。本专题为大家提供php反序列化相关的文章、下载、课程内容,供大家免费下载体验。

    2023.10.11

    2075

    5

    php怎么连接mssql数据库
    php怎么连接mssql数据库

    连接方法:1、通过mssql_系列函数;2、通过sqlsrv_系列函数;3、通过odbc方式连接;4、通过PDO方式;5、通过COM方式连接。想了解php怎么连接mssql数据库的详细内容,可以访问下面的文章。

    2023.10.23

    3688

    4

    php连接mssql数据库的方法
    php连接mssql数据库的方法

    php连接mssql数据库的方法有使用PHP的MSSQL扩展、使用PDO等。想了解更多php连接mssql数据库相关内容,可以阅读本专题下面的文章。

    2023.10.23

    4394

    6

    html怎么上传
    html怎么上传

    html通过使用HTML表单、JavaScript和PHP上传。更多关于html的问题详细请看本专题下面的文章。php中文网欢迎大家前来学习。

    2023.11.03

    3451

    9

    PHP出现乱码怎么解决
    PHP出现乱码怎么解决

    PHP出现乱码可以通过修改PHP文件头部的字符编码设置、检查PHP文件的编码格式、检查数据库连接设置和检查HTML页面的字符编码设置来解决。更多关于php乱码的问题详情请看本专题下面的文章。php中文网欢迎大家前来学习。

    2023.11.09

    4897

    8

    php文件怎么在手机上打开
    php文件怎么在手机上打开

    php文件在手机上打开需要在手机上搭建一个能够运行php的服务器环境,并将php文件上传到服务器上。再在手机上的浏览器中输入服务器的IP地址或域名,加上php文件的路径,即可打开php文件并查看其内容。更多关于php相关问题,详情请看本专题下面的文章。php中文网欢迎大家前来学习。

    2023.11.13

    3842

    8

    sprintf函数用法详解
    sprintf函数用法详解

    sprintf函数的用法:1、格式化字符串;2、指定输出宽度和精度;3、返回值。更多关于sprintf函数用法详解的内容,大家可以阅读下面的文章。

    2023.11.27

    11802

    4

    热门下载

    更多
    网站特效
    /
    网站源码
    /
    网站素材
    /
    前端模板

    精品课程

    更多
    相关推荐
    /
    热门推荐
    /
    最新课程
    墨刀帮助中心
    墨刀帮助中心

    共0课时 | 0人学习

    MyEclipse学习中心
    MyEclipse学习中心

    共0课时 | 0人学习

    Apache Subversion 官方手册
    Apache Subversion 官方手册

    共0课时 | 0人学习