全局作用域必须在booted()或boot()中注册,不可放__construct();需用字符串键名注册以便移除;apply()中只能操作构建器,不可执行查询;绕过时应按需选用withoutglobalscope()、withoutglobalscopes()或withoutscopes()。

全局作用域不是“加个函数就能用”,它一旦注册,就会悄无声息地改掉所有查询结果——包括 find()、first()、关联预加载,甚至 withTrashed() 之外的软删除逻辑。写错位置、漏掉移除、误用上下文,都会导致数据漏查或权限越界。
全局作用域必须在 booted() 或 boot() 里注册
很多人把作用域类写好了,却在模型里漏掉 addGlobalScope() 调用,或者随手塞进构造函数、__construct()、甚至控制器里,结果完全不生效。
-
booted()是推荐位置:模型类加载完成、所有静态属性就绪后触发,适合依赖容器或配置的场景 -
boot()也能用:但若作用域需访问app()或请求上下文(比如租户 ID),得确保这些服务已启动;否则可能报Container is not initialized - 绝对不能放在
__construct():每次new User()都会重复注册,导致条件被叠加多次(如WHERE status = 'active' AND status = 'active') - 注册顺序影响 SQL 嵌套:后注册的作用域会“包”在先注册的外层,类似中间件洋葱模型;若两个作用域都改
tenant_id,后者会覆盖前者
闭包作用域必须带字符串键名,否则无法移除
用 addGlobalScope(function ($builder) { ... }) 看似简洁,但没给键名,后续调用 withoutGlobalScope() 就会失败——Laravel 内部靠这个键名做映射。
- 正确写法:
static::addGlobalScope('active', function (Builder $builder) { ... }) - 移除时必须一致:
User::withoutGlobalScope('active')->get() - 键名需全局唯一:如果两个模型都用了
'active',withoutGlobalScope('active')可能只清掉其中一个,行为不可控 - 闭包里别读
request()或auth():全局作用域在队列任务、命令行或 API 请求中复用时,这些上下文可能为空或错乱
apply() 方法里只能操作 $builder,不能执行查询
作用域的 apply() 是构建器阶段的钩子,不是执行阶段。这里调 $builder->get() 或 $builder->count() 会直接中断链式调用,且可能引发 N+1 或事务异常。
- 只允许调用
where()、whereIn()、orderBy()这类构建器方法 - 不要在
apply()里查数据库、发 HTTP 请求、或访问模型实例的属性($model->id是空的) - 动态值要提前注入:比如多租户作用域需要
tenant_id,应在构造函数传入,而不是在apply()里调tenant()->id - 软删除是特例:它本身也是全局作用域,所以你的自定义作用域会被套在
WHERE deleted_at IS NULL外层;若逻辑冲突(比如你手动加了orWhereNull('deleted_at')),结果可能不符合预期
绕过全局作用域必须用对方法,withoutScopes() 很危险
withoutGlobalScope() 和 withoutScopes() 表面相似,实际影响天差地别。后台导出、数据迁移、调试接口时,选错就等于暴露全部数据。
-
withoutGlobalScope(ActiveScope::class):只剔除指定类的作用域,软删除等其他全局作用域仍生效 -
withoutGlobalScopes():清除所有自定义全局作用域,但保留 Laravel 内置的(如软删除) -
withoutScopes():最激进,连SoftDeletes的WHERE deleted_at IS NULL都干掉,查出来的可能包含已删除记录 - 这两个方法只对当前链式调用生效:写成
User::withoutGlobalScopes()->get()不会影响后续User::all()
最常被忽略的是作用域的“不可绕过性”——它不是装饰器,而是查询构建器的底层约束。多租户、状态隔离这类业务强约束,必须用全局作用域;但凡存在“某些地方需要看到全部数据”的需求,就得提前设计好移除路径,而不是事后硬加 withoutScopes() 来补救。











