← Back to blog

Laravel Eloquent Model Scopes: Write Less, Express More

3 min read

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