Laravel Magazine
Laravel Query Scopes for Cleaner Eloquent Code

Laravel Query Scopes for Cleaner Eloquent Code

Eric Van Johnson ·

If you find yourself writing ->where('status', 'active') in a dozen controllers, query scopes are the fix. A scope gives a common set of constraints a readable name and keeps that logic in the model where it belongs.

Local Scopes

A local scope is just a method on your model prefixed with scope. Laravel strips the prefix when you call it, so scopeActive becomes ->active().

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class Subscriber extends Model
{
    public function scopeActive(Builder $query): void
    {
        $query->where('status', 'active');
    }
}

Now your calling code reads like plain English:

$subscribers = Subscriber::active()->get();

Scopes That Take Arguments

Scopes can accept parameters after the query. This is handy for filters that vary at runtime.

public function scopePublishedBefore(Builder $query, string $date): void
{
    $query->where('published_at', '<', $date);
}

// Usage
Article::publishedBefore('2026-01-01')->get();

Scopes are also chainable, so you can compose them freely:

Subscriber::active()->publishedBefore(now())->latest()->get();

Global Scopes

A global scope applies to every query for a model automatically. This is the classic way to build a "current tenant" or "not archived" constraint you never want to forget.

namespace App\Models\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;

class ActiveScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('status', 'active');
    }
}

Attach it to a model with the #[ScopedBy] attribute, which is the cleanest approach in current Laravel:

use App\Models\Scopes\ActiveScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;

#[ScopedBy([ActiveScope::class])]
class Subscriber extends Model
{
    //
}

When you genuinely need every record, opt out for a single query:

Subscriber::withoutGlobalScope(ActiveScope::class)->get();

A Word of Caution

Global scopes are invisible at the call site, which is exactly why they are powerful and also why they can surprise you. If a report is missing rows, a forgotten global scope is a common culprit. Reach for local scopes by default and save global scopes for constraints that should be truly universal, like multi-tenant isolation.

Scopes are a small feature, but used well they push messy query logic out of your controllers and into the one place it can be tested and reused. Your future self reviewing a controller will thank you.

Further Reading

Stay Updated

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.