Laravel Eloquent Model Scopes: Write Less, Express More
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
- Keep scopes single-responsibility: Each scope should handle one piece of logic
- Use descriptive names:
published()is better thanactive() - Make them chainable: Always return the query builder
- Document parameters: Help your future self understand what goes in
- 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
Building Reactive Forms with Livewire 4: A Complete Guide
Learn how to create dynamic, reactive forms using Livewire 4 that feel responsive and modern without writing a single line of JavaScript.
Advanced Laravel Query Optimization: From N+1 to Perfection
Master the art of writing efficient Laravel queries. Learn about eager loading, query optimization, and how to debug N+1 problems like a pro.
Mastering Livewire 4 Loading States: Creating Smooth User Experiences
Discover how to use wire:loading to create intuitive loading states that make your Livewire components feel buttery smooth and responsive.