Eloquent Builder vs Scopes in Laravel
When to use a custom Eloquent Builder, local scopes, and global scopes for organising reusable query logic in your Laravel application's model layer.
Laravel gives you several ways to organise reusable query logic. Choosing between a custom Eloquent Builder, local scopes, and global scopes is one of the more common decisions you will face. Each tool has a clear purpose.
The Query Builder vs the Eloquent Builder
The Query Builder (DB::table(...)) works at the database level. It returns plain arrays and knows nothing about your models. Use it for raw performance-critical queries, reporting, or operations that do not map neatly onto a single model.
The Eloquent Builder (User::query()) wraps the Query Builder and adds model awareness: it returns model instances, fires events, and supports relationships. This is your default starting point for almost everything.
// Query Builder — returns arrays, no model awareness
$rows = DB::table('users')->where('active', true)->get();
// Eloquent Builder — returns model instances
$users = User::where('active', true)->get();
Local Scopes
Local scopes are methods on your model that you call explicitly. They are prefixed with scope in the model definition but called without the prefix:
class User extends Model
{
public function scopeActive(Builder $query): Builder
{
return $query->where('active', true);
}
public function scopeVerified(Builder $query): Builder
{
return $query->whereNotNull('email_verified_at');
}
public function scopeCreatedAfter(Builder $query, string $date): Builder
{
return $query->where('created_at', '>=', $date);
}
}
// Usage — clean and chainable
$users = User::active()->verified()->createdAfter('2024-01-01')->get();
Local scopes are ideal when:
- The scope is used frequently but only in some queries.
- The logic is simple and tied directly to the model.
- You want IDE-friendly autocompletion without a custom Builder class.
Global Scopes
Global scopes apply automatically to every query on a model. Laravel's own SoftDeletes trait is a global scope — it silently adds WHERE deleted_at IS NULL to every query.
use Illuminate\Database\Eloquent\Scopes\Scope;
class ActiveScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
$builder->where('active', true);
}
}
// Attach it to a model
class User extends Model
{
protected static function booted(): void
{
static::addGlobalScope(new ActiveScope);
}
}
// Now every query on User automatically filters to active users
$users = User::all(); // Only returns active users
// Bypass the scope when you need all records
$allUsers = User::withoutGlobalScope(ActiveScope::class)->get();
Use global scopes sparingly. They change the default behaviour of every query, which can cause subtle bugs if you forget they are there. A good use case is a multi-tenant application where every query must be scoped to the current tenant.
Custom Eloquent Builders
For models with many query methods, a custom Builder class keeps things organised and gives you full type-safety:
class UserBuilder extends \Illuminate\Database\Eloquent\Builder
{
public function active(): self
{
return $this->where('active', true);
}
public function verified(): self
{
return $this->whereNotNull('email_verified_at');
}
public function withPostCount(): self
{
return $this->withCount('posts');
}
}
class User extends Model
{
public function newEloquentBuilder($query): UserBuilder
{
return new UserBuilder($query);
}
}
// Usage
$users = User::query()->active()->verified()->withPostCount()->get();
When to Use Which
| Approach | Best for |
|---|---|
| Local scopes | A handful of reusable filters on a model |
| Global scopes | Constraints that must apply to every single query |
| Custom Builder | Models with many query methods or complex, chainable logic |
| Query Builder | Raw SQL, reporting queries, or non-model operations |
A Common Mistake
Avoid putting too much logic into scopes. If a scope does more than add a where clause or two, it probably belongs in a dedicated action or service class instead.
Newsletter
A weekly newsletter on React, Next.js, AI-assisted development, and engineering. No spam, unsubscribe any time.