Laravel Query Scopes for Cleaner Eloquent Code
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
- Laravel documentation, Eloquent: "Query Scopes" (laravel.com/docs/13.x/eloquent#query-scopes)