Local & Global Scopes
Bottle up reusable query logic — and apply cross-cutting filters to every query automatically.
Scopes are how you stop copy-pasting the same where() clauses across a codebase. There are two kinds, and knowing which to reach for is a senior instinct.
Local scopes — opt in
A method you call explicitly. Great for readable, composable query fragments.
// Post.php
public function scopePublished($query)
{
return $query->where('published', true);
}
// Post::published()->latest()->get();Laravel 12 also supports the attribute form #[Scope] on a plain method, which some teams find cleaner than the scope prefix.
Global scopes — opt out
Applied to every query for the model, automatically. Perfect for multi-tenancy ("only this team's rows") or soft-delete-style filters. The danger is invisibility: a scope you forget about can make rows "disappear" and cost hours of debugging.
Example
<?php
namespace App\Models\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class TenantScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
// Assumes a resolved current tenant in the container.
if ($tenantId = app('tenant')?->id) {
$builder->where($model->getTable() . '.team_id', $tenantId);
}
}
}
// --- Attaching it + a local scope on the model ---
namespace App\Models;
use App\Models\Scopes\TenantScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
#[ScopedBy([TenantScope::class])] // global scope, applied to every query
class Invoice extends Model
{
// Local scope: opt-in, composable, chainable.
public function scopeOverdue(Builder $query): Builder
{
return $query->where('due_at', '<', now())
->where('status', '!=', 'paid');
}
}
// --- Usage ---
Invoice::overdue()->get(); // tenant-filtered + overdue
Invoice::withoutGlobalScope(TenantScope::class) // admin / cross-tenant report
->overdue()
->get();When to use it
- A Product model defines a scopeActive() method so Product::active()->get() filters to enabled products everywhere without repeating the where clause.
- An Order model's scopeForUser() accepts a user parameter so any query can be scoped to a specific customer in one readable chainable call.
- A global scope on the Post model automatically appends WHERE tenant_id = ? to every query, enforcing multi-tenancy transparently.
More examples
Local query scope
Local scopes are prefixed with 'scope' in the method name and called without the prefix, enabling readable fluent query chains.
class Product extends Model
{
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function scopeInPriceRange($query, float $min, float $max)
{
return $query->whereBetween('price', [$min, $max]);
}
}
// Usage
Product::active()->inPriceRange(10, 100)->get();Global scope with a class
A global scope class automatically appends the tenant filter to every query on the model, enforcing multi-tenancy without repeating the condition.
// app/Scopes/TenantScope.php
class TenantScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
$builder->where('tenant_id', app('tenant')->id);
}
}
// app/Models/Post.php
protected static function booted(): void
{
static::addGlobalScope(new TenantScope());
}Remove a global scope when needed
withoutGlobalScope() lets admin queries or migrations opt out of automatic filters that normal application queries rely on.
// Bypass the TenantScope for an admin report
$allPosts = Post::withoutGlobalScope(TenantScope::class)->get();
// Remove all global scopes
$allPosts = Post::withoutGlobalScopes()->get();
Discussion