如何为Laravel模型定义全局作用域(Global Scope)? (自动应用查询条件)

11次阅读

全局作用域laravel模型层面自动注入的查询条件,只要使用该模型进行Eloquent查询(where、get、find等),就会无感地加上你定义的约束。

如何为Laravel模型定义全局作用域(Global Scope)? (自动应用查询条件)

什么是全局作用域,它在什么场景下必须用

全局作用域是 Laravel 模型层面自动注入的查询条件,只要使用该模型进行 Eloquent 查询(wheregetfind 等),就会无感地加上你定义的约束。典型场景包括:多租户系统中自动过滤 tenant_id,软删除之外的业务级状态隔离(如只查 status = 'active'),或租户内数据自动绑定当前用户所属组织。

注意:它不是中间件或服务层逻辑,而是直接嵌入到 Eloquent 的查询构建器中,对所有静态/实例查询生效(除非显式移除)。

如何定义一个可复用的全局作用域类

推荐用独立类实现 IlluminatedatabaseEloquentScope 接口,避免闭包作用域带来的测试与维护问题。关键点在于 apply() 方法必须调用 $builder->where() 或类似方法修改查询构建器。

  • 类必须实现 apply() 方法,接收 $builder$model 两个参数
  • 不要在 apply() 中调用 $builder->get() 或执行查询——那会破坏链式调用
  • 若需动态值(如当前租户 ID),应在作用域构造时传入,而非在 apply() 中读取全局状态(如 session()auth()),否则会导致队列任务或 API 请求上下文错乱
namespace AppScopes;  use IlluminateDatabaseEloquentBuilder; use IlluminateDatabaseEloquentModel; use IlluminateDatabaseEloquentScope;  class TenantScope implements Scope {     protected $tenantId;      public function __construct($tenantId)     {         $this->tenantId = $tenantId;     }      public function apply(Builder $builder, Model $model)     {         $builder->where('tenant_id', $this->tenantId);     } }

如何在模型中注册全局作用域

在模型的 booted() 方法中调用 Static::addGlobalScope()。注意不是 boot(),因为 booted() 是模型类加载完成后的钩子,确保作用域注册时机正确。

  • 若作用域类带构造参数(如上例的 $tenantId),必须在 booted() 中获取该值(例如从请求上下文、服务容器或配置中读取)
  • 多个全局作用域按注册顺序叠加,但彼此独立;若逻辑冲突(如都改 where 同一字段),后注册的会覆盖先注册的
  • 不能在模型构造函数中注册,否则每次 new Model() 都会重复添加,导致作用域被多次应用
use AppScopesTenantScope;  class Post extends Model {     protected static function booted()     {         $tenantId = request()->user()?->tenant_id ?? config('app.default_tenant');         static::addGlobalScope(new TenantScope($tenantId));     } }

如何临时取消某个全局作用域

withoutGlobalScope() 可跳过单个作用域,withoutGlobalScopes() 则跳过全部。这在后台管理、数据迁移或调试时非常必要——否则连 php artisan tinker 查数据都会被拦截。

  • withoutGlobalScope(TenantScope::class):只移除指定类的作用域
  • withoutGlobalScopes():彻底清空所有全局作用域(含 Laravel 自带的软删除)
  • 不支持按名字移除(如字符串标识),必须传类名或作用域实例
  • 这个方法只对当前查询链有效,不影响后续其他查询
// 跳过租户作用域查所有文章 $allPosts = Post::withoutGlobalScope(TenantScope::class)->get();  // 完全绕过所有全局作用域(含软删除) $forceAll = Post::withoutGlobalScopes()->get();

真正容易被忽略的是:全局作用域对 count()exists()pluck() 等所有基于 Builder 的方法都生效,而不仅是 get()。如果忘记这点,在统计总数或判断是否存在时可能得到错误结果。

text=ZqhQzanResources