Laravel

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

ApproachBest for
Local scopesA handful of reusable filters on a model
Global scopesConstraints that must apply to every single query
Custom BuilderModels with many query methods or complex, chainable logic
Query BuilderRaw 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.

← Older
Form Request Validation Patterns in Laravel
Newer →
Custom Collections in Laravel

Newsletter

A weekly newsletter on React, Next.js, AI-assisted development, and engineering. No spam, unsubscribe any time.