作用域是查询逻辑的封装契约,非语法糖;本地作用域须为public方法、首参$query、显式return;全局作用域应在booted()中注册,慎用闭包;关联筛选需withwherehas,避免orwhere滥用。

作用域不是语法糖,是查询逻辑的封装契约——写错签名、漏掉返回、混用 orWhere,查出来的数据就可能出错。
本地作用域必须是 public 方法,且第一个参数固定为 $query
常见错误现象:Call to undefined method App\Models\User::active() 或链式调用中断(比如 ->active()->orderBy(...) 报错)。这通常是因为:方法没加 public 修饰符、首参不是 $query、或者忘了 return $query。
- 定义时签名必须是
public function scopeActive($query),不能是static(Laravel 10+ 允许 static,但非必需;非 static 更直观,也兼容所有版本) -
$query类型是Illuminate\Database\Eloquent\Builder,不是$this,也不是模型实例 - 必须显式
return $query,否则后续链式调用会丢失上下文 - 调用时写
->active(),不能省略括号;写成->active会直接报错
带参数的本地作用域要防御空值,避免生成无效 where
使用场景:按关键词搜索、按状态筛选、按时间范围过滤——这些条件往往来自请求参数,可能为空或缺失。硬写 where('keyword', $kw) 会导致 SQL 报错或语义错误。
- 参数应设默认值,例如
scopeKeyword($query, $kw = '') - 内部用
if (! blank($kw)) { $query->where('title', 'like', "%{$kw}%"); }控制是否追加条件 - 不要依赖全局变量或
request(),所有输入必须显式传入,保证作用域可测试、可复用 - 多个参数顺序必须紧贴
$query,如scopeBetween($query, $start, $end),调用为->between('2026-01-01', '2026-04-30')
全局作用域注册必须在 booted() 中,闭包方式慎用于多模型共享
常见错误现象:全局作用域只生效一次、部分查询漏掉条件、测试环境下条件意外叠加——根源常在于注册时机或作用域类构造方式。
- 必须用
protected static function booted(),不是boot();booted()是模型类完全加载后的钩子,确保作用域只注册一次 - 闭包方式(
addGlobalScope('tenant', function ($builder, $model) { ... }))适合单模型轻量逻辑;但无法被其他模型复用,也不易单元测试 - 若作用域类需传参(如租户 ID),应在
booted()中获取,例如static::addGlobalScope(new TenantScope(request()->header('X-Tenant-ID'))) - 多个全局作用域按注册顺序叠加,但 WHERE 条件是 AND 合并;若两个作用域都改
status字段,后注册的会覆盖前一个
作用域不自动下推到关联查询,withWhereHas 才是正确解法
容易踩的坑:写 User::active()->with('posts')->get(),以为 active() 也会筛出「用户启用且文章启用」的数据——其实 active() 只作用于 User 表,posts 关联查的是全部。
- 想约束关联模型,必须显式用
withWhereHas('posts', function ($q) { $q->active(); }) -
has('posts.active')仅判断是否存在匹配的关联,不加载数据;需加载 + 筛选时,withWhereHas是唯一可靠方式 - 作用域内避免直接写
orWhere;它会脱离当前 AND 分组,导致逻辑爆炸。真需要 OR,用闭包包裹:$query->where(function ($q) { $q->where(...)->orWhere(...); })
最常被忽略的一点:全局作用域一旦注册,连 count()、exists()、pluck() 这些“非 get”查询都会生效——它不挑操作类型,只认模型。调试时别只看 get() 结果,得查原始 SQL 才能确认条件是否真被加上了。











