paulund

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.