必须用 whereHas 逐层过滤嵌套关联以确保权限安全:先在 Category 上 whereHas('subcategories') 并嵌套 whereHas('products') 做权限判断,再 with(['subcategories.products']) 预加载;禁用 with() 混用作用域,复杂权限用 fromSub() 构建子查询。

在 Laravel 中对嵌套关联数据做权限级过滤时,必须确保底层关系(如 user → posts → comments)每一层都受当前用户权限约束,否则未授权数据可能被意外加载或返回。
用 whereHas 拉取带权限的完整嵌套结构
当需要从 Category 出发,只查出“当前用户有查看权限”的 Category → Subcategory → Product 树形结构时,不能依赖作用域预加载,而要用 whereHas 逐层下钻过滤。
第一步:在 Category 模型上写 whereHas('subcategories'),闭包内再嵌套 whereHas('products'),并在最内层加权限判断。
第二步:闭包中调用 Auth::id() 或 $request->user()->id 获取当前用户,然后关联到 products 表的 visibility_policy 字段或中间权限表(如 product_access)。
第三步:使用 with(['subcategories.products']) 一次性预加载,确保 N+1 被消除,且所有 loaded 数据都已通过 whereHas 的权限校验——【这是唯一能保证返回结果 100% 符合权限规则的方式】。
避免 with() + 作用域混用导致权限失效
方法一:禁止在调用 withActiveSubscriptions() 后再链式调用 with('subscriptions.plan')。
方法二:把全部嵌套路径作为参数传入作用域,例如 ->withActiveSubscriptions(['subscriptions.plan', 'subscriptions.invoice'])。
方法三:若 plan 层还需额外权限控制(如仅 GOLD 用户可见),则必须改用 whereHas('subscriptions', fn ($q) => $q->where('active', true)->whereHas('plan', fn ($p) => $p->where('type', 'GOLD'))) → with(['subscriptions.plan'])。
注意:Eloquent 不合并同名关系的闭包条件,两次 with() 调用 subscriptions 会导致第一次的 active = true 被覆盖。
动态组合多层权限过滤的子查询写法
当权限逻辑复杂到无法用 whereHas 清晰表达(例如需按用户角色 + 时间窗口 + 地域白名单联合判定),就该用 fromSub() 构建权限子查询临时表。
先用 DB::table('products') → selectRaw('product_id, MAX(CASE WHEN ... THEN 1 ELSE 0 END) as can_view') → groupBy('product_id') 构建权限视图。
再把这个子查询作为 fromSub() 的第一个参数,主查询 join 它,并在 where 中加 can_view = 1。
最后用 Eloquent 的 from() 方法包装该子查询结果,转成模型集合:Product::from($subQuery)->with(['subcategory.category'])->get()。


















