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

为什么直接递归查数据库做菜单树容易出问题
因为每次展开子菜单都要重新查一次数据库,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 "
- {$items}
从数据库构建组合树时最容易踩的坑
别在循环里 new 一堆 MenuGroup 再手动 add() —— 这样无法复用已存在的分组实例,导致相同父级被重复创建,权限判断错乱。
- 用递归查询 + 一次 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免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











