Eloquent Query Scopes
Eloquent Query Scopes: Tips & Tricks
Eloquent Query Scopes: Tips & Tricks
Tip: Chain Scopes for Readability
Post::published()->popular()->recent()->get();
Each scope returns the query builder, so they chain naturally.
Gotcha: Global Scopes Apply Automatically
static::addGlobalScope('active', function ($builder) {
$builder->where('active', true);
});
Every query on this model gets the scope. Use withoutGlobalScope() to bypass.
Tip: Dynamic Scopes Accept Parameters
public function scopeOfType(Builder $query, string $type): Builder
{
return $query->where('type', $type);
}
// Usage:
Post::ofType('article')->get();
Tip: Use Scopes in Relationships
public function publishedPosts(): HasMany
{
return $this->hasMany(Post::class)->published();
}
Gotcha: Scopes Don't Work with find()
Post::published()->find(1); // Works
Post::find(1)->published(); // Doesn't work — find() returns a model
Tip: Scope Reusability Across Models
trait HasPublishedScope
{
public function scopePublished(Builder $query): Builder
{
return $query->whereNotNull('published_at');
}
}
Tip: Scope for Ordering
public function scopeLatest(Builder $query): Builder
{
return $query->orderByDesc('created_at');
}
Gotcha: Global Scope Removal
Post::withoutGlobalScope('active')->get();
Post::withoutGlobalScopes()->get(); // Remove all
Tip: Use cursor() for Memory-Neutral Iteration
When exporting 100K rows, get() loads everything into memory. cursor() uses yield and keeps memory flat regardless of row count. Perfect for artisan commands.
Tip: whereHas() vs load() — Two Different Things
whereHas() filters the parent query by relationship existence. load() eager-loads relationships AFTER the query. Mixing them up is a common source of logic bugs.
Gotcha: withCount() Adds a Subquery
withCount('comments') runs a correlated subquery on every row. On large tables, this can be slower than a separate query. Profile before relying on it.
Senior Insight
Query scopes are Laravel's answer to reusable query logic, and they're excellent when used correctly. The mistake I see most often: scopes that accept optional parameters with complex default logic, making them unpredictable. I follow the principle of least surprise — each scope should do exactly one filtering operation, and the name should describe exactly what it does. active() returns active records. recent() orders by created_at. Never combine filtering and ordering in one scope.
Source: Laravel Docs (https://laravel.com/docs/eloquent), Laravel News (https://laravel-news.com/), Freek.dev (https://freek.dev/tags/eloquent)