Scope (Eloquent) — reusable WHERE clause fragments attached to a model
Concept
Scope (Eloquent) — a reusable query constraint fragment attached to a model. Instead of repeating ->where('active', true) everywhere, define it once and call ->active() on any query.
Two types of scopes:
Local scopes: Named, explicitly called. Defined as scopeMethodName(Builder $query). Called as ->methodName() (without the scope prefix). Chainable.
Global scopes: Applied to EVERY query on the model, automatically. You never have to call them. Soft deletes use a global scope to add WHERE deleted_at IS NULL to every query automatically.
Local scope naming: The method is public function scopeActive(Builder $query). You call it as User::active()->get(). Laravel strips the scope prefix.
Parameterized scopes: Local scopes can accept parameters. scopeOfRole(Builder $query, string $role) → User::ofRole('admin')->get().
Global scope removal: User::withoutGlobalScope(ActiveScope::class)->get() — bypasses a specific global scope. withoutGlobalScopes() bypasses ALL global scopes.
Soft deletes global scope: SoftDeletingScope is a global scope that adds WHERE deleted_at IS NULL. User::withTrashed() removes it. User::onlyTrashed() inverts it.
Code Example
<?php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
class User extends Model
{
use \Illuminate\Database\Eloquent\SoftDeletes; // adds global scope automatically
// LOCAL SCOPE — explicitly called, reusable WHERE fragment
public function scopeActive(Builder $query): void
{
$query->where('active', true);
}
public function scopeVerified(Builder $query): void
{
$query->whereNotNull('email_verified_at');
}
// PARAMETERIZED local scope
public function scopeOfRole(Builder $query, string $role): void
{
$query->where('role', $role);
}
public function scopeCreatedAfter(Builder $query, \Carbon\Carbon $date): void
{
$query->where('created_at', '>=', $date);
}
}
// Usage — readable, composable
$activeAdmins = User::active() // scopeActive
->ofRole('admin') // scopeOfRole('admin')
->verified() // scopeVerified
->createdAfter(now()->subDays(30)) // scopeCreatedAfter
->get();
// Generated SQL:
// WHERE active = 1
// AND role = 'admin'
// AND email_verified_at IS NOT NULL
// AND created_at >= '2025-10-01'
// AND deleted_at IS NULL ← added by SoftDeletingScope global scope
// GLOBAL SCOPE — applied to ALL queries automatically
class PublishedScope implements \Illuminate\Database\Eloquent\Scope
{
public function apply(Builder $builder, Model $model): void
{
$builder->where('published', true);
}
}
class Article extends Model
{
protected static function booted(): void
{
static::addGlobalScope(new PublishedScope());
// OR inline:
static::addGlobalScope('published', fn(Builder $b) => $b->where('published', true));
}
}
Article::all(); // WHERE published = true — always! (global scope)
Article::find(1); // WHERE id = 1 AND published = true
// Bypassing global scope
Article::withoutGlobalScope('published')->get(); // all articles, published or not
Article::withoutGlobalScope(PublishedScope::class)->get(); // same using class
// ELOQUENT'S BUILT-IN GLOBAL SCOPE: SoftDeletes
$users = User::all(); // WHERE deleted_at IS NULL (hidden deleted users)
$users = User::withTrashed()->all(); // includes deleted users
$users = User::onlyTrashed()->all(); // ONLY deleted users
$user = User::find(1); // automatically: AND deleted_at IS NULL