Back to blog

Laravel Eloquent Model Scopes: Write Less, Express More

Laravel Eloquent Scopes Query Building

Scopes are one of Laravel's most underutilized features. They let you encapsulate query logic in reusable, chainable methods that make your code more readable and maintainable.

Local Scopes

A local scope is a method on your model that returns a query builder instance, prefixed with scope:

class Post extends Model
{
    public function scopePublished($query)
    {
        return $query->where('published', true);
    }

    public function scopeRecentFirst($query)
    {
        return $query->orderBy('published_at', 'desc');
    }
}

Now you can use these naturally in your queries:

$posts = Post::published()->recentFirst()->paginate(15);

This is infinitely more readable than:

$posts = Post::where('published', true)
    ->orderBy('published_at', 'desc')
    ->paginate(15);

Scopes with Parameters

Pass parameters to make scopes even more flexible:

public function scopeFromAuthor($query, User $author)
{
    return $query->where('author_id', $author->id);
}

public function scopePublishedAfter($query, Carbon $date)
{
    return $query->where('published_at', '>=', $date);
}

Usage:

$author = User::find(1);
$posts = Post::fromAuthor($author)
    ->publishedAfter(now()->subMonth())
    ->get();

Global Scopes

Sometimes you want a scope applied to every query automatically. Use global scopes:

class SoftDeleteScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->whereNull('deleted_at');
    }
}

class Post extends Model
{
    protected static function booted(): void
    {
        static::addGlobalScope(new SoftDeleteScope());
    }
}

Now every query on Post automatically excludes deleted records:

$posts = Post::all(); // Deleted posts are excluded automatically

$posts = Post::withoutGlobalScopes()->get(); // Include deleted posts

Anonymous Global Scopes

Laravel also supports anonymous global scopes for simpler cases:

protected static function booted(): void
{
    static::addGlobalScope('archived', function (Builder $builder) {
        $builder->where('archived', false);
    });
}

Composing Complex Queries

Scopes shine when building complex queries. Imagine filtering posts by multiple criteria:

$posts = Post::published()
    ->fromAuthor($author)
    ->publishedAfter(now()->subMonth())
    ->withCount('comments')
    ->with('author')
    ->recentFirst()
    ->paginate(15);

This reads like natural English and is infinitely more maintainable than a giant where clause.

Real-World Example: Analytics Dashboard

class PageView extends Model
{
    public function scopeToday($query)
    {
        return $query->where('created_at', '>=', now()->startOfDay());
    }

    public function scopeThisWeek($query)
    {
        return $query->where('created_at', '>=', now()->startOfWeek());
    }

    public function scopeFromPage($query, string $slug)
    {
        return $query->where('page_slug', $slug);
    }

    public function scopeCountByPage($query)
    {
        return $query->groupBy('page_slug')
            ->selectRaw('page_slug, count(*) as views')
            ->orderByDesc('views');
    }
}

// Usage
$weeklyStats = PageView::thisWeek()
    ->countByPage()
    ->get();

Best Practices

  1. Keep scopes single-responsibility: Each scope should handle one piece of logic
  2. Use descriptive names: published() is better than active()
  3. Make them chainable: Always return the query builder
  4. Document parameters: Help your future self understand what goes in
  5. Test them: Scopes are logic and should have tests

Conclusion

Scopes transform your queries from scattered, hard-to-read where clauses into clean, composable expressions. They're a cornerstone of writing maintainable Laravel applications. Start using them today, and your code will thank you tomorrow.

You Might Also Like