Laravel全局作用域中能否使用模型属性?权限控制场景求助
解决Laravel全局作用域中无法获取模型属性的问题(限制Encounter模型访问权限)
问题背景
我想限制Encounter模型仅对特定用户组开放,同时屏蔽组内禁止的服务类型,打算用全局作用域实现,但代码里用$this->service_type_id报错,因为全局作用域里拿不到模型属性。
当前错误代码:
protected static function boot() { parent::boot(); static::addGlobalScope('status', function (Builder $builder) { $builder->whereHas('groups.users', fn($q) => $q->where('user_id', Auth::user()->id)) ->whereDoesntHave('groups.prohibitedServiceTypes', fn($q) => $q->where('service_type_id', $this->service_type_id)); }); }
关联关系:Encounter和Groups是多对多,Groups和Users、ProhibitedServiceTypes也是多对多。目标是筛选出当前用户所在组关联的、且service_type_id不在组禁止列表中的Encounter,对应的原生SQL如下:
SELECT * FROM encounters e JOIN group_encounter ge ON (e.id = ge.encounter_id) JOIN groups g ON (g.id=ge.group_id) JOIN group_user gu ON (gu.group_id=g.id) WHERE gu.user_id = <current user> AND NOT EXISTS (SELECT * FROM group_prohibited_service_types WHERE service_type_id = <current service type>)
问题根源
全局作用域是在构建查询阶段执行的,此时还没有生成具体的Encounter模型实例,所以$this根本不存在,自然拿不到service_type_id属性。要实现字段之间的关联对比,得用whereColumn来关联主表和关联表的字段。
解决方案
方案一:用Eloquent关联方法(推荐,符合Laravel规范)
use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Auth; protected static function boot() { parent::boot(); static::addGlobalScope('accessible', function (Builder $builder) { // 未登录用户直接返回空结果 if (!Auth::check()) { $builder->whereRaw('1=0'); return; } $userId = Auth::user()->id; // 动态获取Encounter表名,避免硬编码 $encounterTable = $builder->getModel()->getTable(); $builder // 筛选当前用户所在组关联的Encounter ->whereHas('groups', function ($groupQuery) use ($userId) { $groupQuery->whereHas('users', fn($userQ) => $userQ->where('user_id', $userId)); }) // 排除那些所在组禁止了其service_type_id的Encounter ->whereDoesntHave('groups', function ($groupQuery) use ($encounterTable) { $groupQuery->whereHas('prohibitedServiceTypes', fn($prohibitedQ) => $prohibitedQ->whereColumn('service_type_id', "{$encounterTable}.service_type_id") ); }); }); }
方案二:贴近原生SQL的实现
如果更习惯原生SQL的逻辑,可以用join和whereNotExists来实现,和你给出的SQL几乎一致:
use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; protected static function boot() { parent::boot(); static::addGlobalScope('accessible', function (Builder $builder) { if (!Auth::check()) { $builder->whereRaw('1=0'); return; } $userId = Auth::user()->id; $encounterTable = $builder->getModel()->getTable(); $builder ->join('group_encounter', "{$encounterTable}.id", '=', 'group_encounter.encounter_id') ->join('groups', 'groups.id', '=', 'group_encounter.group_id') ->join('group_user', 'groups.id', '=', 'group_user.group_id') ->where('group_user.user_id', $userId) ->whereNotExists(function ($subquery) use ($encounterTable) { $subquery->select(DB::raw(1)) ->from('group_prohibited_service_types') ->whereColumn('group_prohibited_service_types.service_type_id', "{$encounterTable}.service_type_id") ->whereColumn('group_prohibited_service_types.group_id', 'groups.id'); }) // 确保只查询Encounter的字段,避免join带来的重复字段问题 ->select("{$encounterTable}.*"); }); }
关键注意事项
- 必须判断用户登录状态:未登录时
Auth::user()为null,直接返回空结果避免报错。 - 动态获取表名:用
$builder->getModel()->getTable()代替硬编码表名,符合Laravel的可维护性要求。 - 用whereColumn做字段对比:这是实现“当前Encounter的service_type_id不在组禁止列表”的核心,它会对比两个表的字段值,而不是传固定值。
内容的提问来源于stack exchange,提问作者mankowitz
相关产品推荐
相关产品推荐

