How local and global scopes work in Eloquent, with a look at SoftDeletingScope from the framework internals, plus practical patterns for multi-tenancy and publish/draft filtering.
Call the scope as a method on the model. You can chain multiple scopes.
use App\Models\Post;// Only published posts$posts = Post::published()->get();// Published AND popular, ordered by newest$posts = Post::published()->popular()->latest()->get();
use App\Models\Scopes\ActiveScope;// Remove one specific scopeUser::withoutGlobalScope(ActiveScope::class)->get();// Remove a closure-based scope by nameUser::withoutGlobalScope('active')->get();// Remove all global scopesUser::withoutGlobalScopes()->get();// Remove multiple scopesUser::withoutGlobalScopes([ActiveScope::class, TenantScope::class])->get();// Remove all scopes except the ones you listUser::withoutGlobalScopesExcept([TenantScope::class])->get();
Laravel’s SoftDeletes trait is a good example of how global scopes are used in practice.
// src/Illuminate/Database/Eloquent/SoftDeletingScope.phpclass SoftDeletingScope implements Scope{ protected $extensions = [ 'Restore', 'RestoreOrCreate', 'CreateOrRestore', 'WithTrashed', 'WithoutTrashed', 'OnlyTrashed', ]; public function apply(Builder $builder, Model $model) { $builder->whereNull($model->getQualifiedDeletedAtColumn()); } public function extend(Builder $builder) { foreach ($this->extensions as $extension) { $this->{"add{$extension}"}($builder); } $builder->onDelete(function (Builder $builder) { $column = $this->getDeletedAtColumn($builder); return $builder->update([ $column => $builder->getModel()->freshTimestampString(), ]); }); }}
See the withTrashed() implementation
withTrashed() simply calls withoutGlobalScope($this) — it removes the SoftDeletingScope itself so soft-deleted records are included.
protected function addWithTrashed(Builder $builder){ $builder->macro('withTrashed', function (Builder $builder, $withTrashed = true) { if (! $withTrashed) { return $builder->withoutTrashed(); } return $builder->withoutGlobalScope($this); });}
onlyTrashed() also removes the scope, then adds a whereNotNull('deleted_at') constraint.
protected function addOnlyTrashed(Builder $builder){ $builder->macro('onlyTrashed', function (Builder $builder) { $model = $builder->getModel(); $builder->withoutGlobalScope($this)->whereNotNull( $model->getQualifiedDeletedAtColumn() ); return $builder; });}
The Scope interface does not declare an extend method, but Eloquent’s builder calls it automatically when it exists. Use extend to attach macros to the builder when your scope is applied.
When adding columns inside a global scope, always use addSelect rather than select. Using select overwrites any columns the caller already selected.
// Bad: overwrites the caller's select clausepublic function apply(Builder $builder, Model $model): void{ $builder->select('id', 'tenant_id', 'name');}// Good: appends to the existing select clausepublic function apply(Builder $builder, Model $model): void{ $builder->addSelect('tenant_id');}