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

Example · php
<?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.

Example · php
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.

Example · php
// 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.

Example · php
// Bypass the TenantScope for an admin report
$allPosts = Post::withoutGlobalScope(TenantScope::class)->get();

// Remove all global scopes
$allPosts = Post::withoutGlobalScopes()->get();

Discussion

  • Be the first to comment on this lesson.