Eloquent Builder vs Scopes
Eloquent Builder vs Scopes
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.