局部作用域必须以scope开头、首参为$query并return $query,否则中断链式调用;全局作用域自动生效且无法访问请求上下文,适用于软删除、多租户等不可绕过场景。

全局作用域自动生效,局部作用域必须显式调用——选错类型会导致数据漏查或误过滤,不是语法问题,而是权限和业务逻辑层面的事故。
局部作用域怎么定义才不会中断链式调用
局部作用域是模型里的普通方法,但必须满足两个硬性条件:方法名以 scope 开头,且第一个参数固定为 $query(即 Builder 实例),最后必须 return $query。
常见错误包括:
- 写成
active()而不是scopeActive(),调用时直接报Call to undefined method - 在作用域里调用
$this->where()——$this是模型实例,没有where方法 - 返回
$query->get()或数组,导致后续无法接->orderBy()、->paginate() - 带参数时把参数放第一位,比如
scopePublished($year),正确应为scopePublished($query, $year)
示例:
public function scopePublishedAfter($query, $date)
{
return $query->where('published_at', '>=', \Carbon\Carbon::parse($date)->startOfDay());
}
调用:Post::publishedAfter('2024-01-01')->orderBy('id')->get() —— 链路完整,可继续拼接。
全局作用域为什么不能访问请求上下文
全局作用域在模型 boot() 中注册,所有查询(包括 find()、first()、关联预加载)都会无条件触发。它的 apply() 方法只接收 $builder 和 $model,拿不到 request、auth 或路由参数。
这意味着:
- 不能在全局作用域里写
tenant()->id,除非你提前把租户 ID 存到静态变量或容器中(不推荐) - 不能根据当前用户角色动态加
where('visibility', 'public'),这种逻辑必须留在局部作用域或服务层 - 软删除(
whereNull('deleted_at'))能用,是因为它不依赖运行时上下文
典型注册方式:
protected static function boot()
{
parent::boot();
static::addGlobalScope('active', function (Builder $builder) {
$builder->where('status', 'active');
});
}
什么时候该用全局作用域,而不是一堆局部作用域
判断标准不是“复用次数”,而是“是否允许被绕过”。全局作用域一旦注册,就覆盖所有查询路径;局部作用域只在你明确调用时才生效。
适用全局作用域的场景:
- 软删除:你不希望任何地方能意外查出已删除记录
- 多租户隔离(
where('tenant_id', ...)):必须 100% 强制,漏写一次就可能泄露其他租户数据
该用局部作用域的场景:
- 「仅管理员可见」这类权限逻辑:普通用户查列表不该带这个条件,管理员后台才需要
- 按时间范围筛选、状态组合(草稿/已发布/审核中):不同页面需求不同,不能一刀切
- 搜索类条件(
scopeSearch($query, $term)):显然不能全局加上
混用时注意:全局作用域会包裹局部作用域,顺序不可控;若需临时跳过,用 withoutGlobalScopes() 或指定类名,如 withoutGlobalScope(TenantScope::class)。
作用域里别做这些事
作用域只负责拼 SQL 条件,不是执行逻辑的入口。以下操作会埋坑:
- 在作用域里调用
DB::table()或额外查询 —— 它本应是纯构建器操作,嵌套查询会破坏性能预期 - 手动处理分页(
->paginate())或 eager loading(->with())—— 这些该由调用方决定 - 对参数不做校验,比如传入空字符串却直接塞进
where(),导致 SQL 报错或结果异常 - 在局部作用域里写
static::all()或$this->fresh()—— 完全脱离了构建器上下文
最易被忽略的一点:全局作用域会影响 count()、exists()、increment() 等所有基于查询构建器的操作,而不仅是 get()。这点在写数据统计或原子更新时尤其关键。











