Back to Blog
Lesson 42 of the Advanced Laravel: Architecture, Scaling & Performance course
LaravelJune 28, 20264 min read

Advanced Eloquent Scopes: Building Complex, Reusable Query Filters

Master advanced Eloquent scopes to encapsulate complex business logic, chain query filters, and maintain clean, expressive models in your Laravel SaaS platform.

LaravelEloquentClean CodeArchitectureRefactoringphpbackend

Previously in this course, we explored database query caching layers to reduce load on our primary instances. In this lesson, we shift our focus from the database layer back to the application layer to address code maintainability. We will master Advanced Eloquent Scopes, enabling you to encapsulate complex business logic into reusable, chainable query constraints.

The Problem with Query Bloat

In a high-traffic SaaS platform, controllers often become cluttered with complex where clauses, orWhere logic, and date filtering. When this logic is repeated across different endpoints or services, it violates the DRY principle and makes cross-cutting changes (like updating a business rule) a nightmare.

Eloquent scopes allow us to move this logic directly into our models, transforming messy query builders into readable, intent-revealing method chains. As we've discussed when refactoring monolithic components, moving logic closer to the data is a cornerstone of clean architecture.

Building Complex Local Scopes

Local scopes are defined in your model and prefixed with scope. They receive the $query instance as their first argument, allowing you to manipulate the underlying Builder object.

For our SaaS project, let's look at an Invoice model that needs to filter by status, date ranges, and payment types.

PHP
namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class Invoice extends Model
{
    #6A9955">/**
     * Scope a query to only include overdue invoices.
     */
    public function scopeOverdue(Builder $query): void
    {
        $query->where('due_at', '<', now())
              ->where('status', 'pending');
    }

    #6A9955">/**
     * Scope a query to filter by amount range.
     */
    public function scopeWithAmountBetween(Builder $query, float $min, float $max): void
    {
        $query->whereBetween('amount', [$min, $max]);
    }
}

By defining these as scopes, your controller logic simplifies from raw SQL-like builders to expressive business language:

PHP
#6A9955">// Usage in a Service or Controller
$invoices = Invoice::overdue()
    ->withAmountBetween(100, 500)
    ->get();

Chaining Scopes for Business Logic

The real power of Eloquent scopes lies in their composability. When building a reporting engine, you can chain multiple scopes to construct complex SQL dynamically based on user input.

However, be careful with state. Always ensure your scopes return void or the $query instance, and never execute the query inside the scope itself (e.g., don't call ->get() or ->first() inside a scope).

The Dynamic Filter Pattern

When dealing with complex filters, use a "Filter" class or a dedicated trait to avoid bloating your model. If you are interested in how this integrates with wider model cleanup, see mastering Laravel traits for cleaner Eloquent models.

Global vs. Local Scopes

While local scopes are for specific queries, global scopes apply to every query made on a model. As seen in our work on multi-tenant security, global scopes are essential for enforcing data isolation.

Scope TypeUse CaseImplementation
LocalAd-hoc queries, business-specific filtersscopeName($query, ...)
GlobalSoft deletes, multi-tenancy, data isolationimplements Scope class

Hands-on Exercise

  1. Open your Subscription model in the SaaS project.
  2. Create a local scope scopeActiveTrial that filters for users currently in their trial period (trial_ends_at is in the future).
  3. Create a scope scopeExpiringInDays(Builder $query, int $days) that finds subscriptions expiring within a specific window.
  4. Chain these together in a controller to fetch all users on a trial that expires in the next 3 days.

Common Pitfalls

  • Executing the Query: Never trigger the query inside a scope. A scope should only modify the Builder instance.
  • Scope Overload: Don't turn your model into a "God Object." If a scope is only used by one specific report, consider if it belongs in a dedicated Query Object or a Service class instead.
  • Ignoring Indexes: Just because a scope makes the code clean doesn't mean it's performant. Always check the generated SQL to ensure you aren't filtering on unindexed columns, as covered in advanced indexing strategies.

Recap

Advanced Eloquent scopes are a primary tool for maintaining readability in large Laravel applications. By encapsulating domain logic, you ensure that query constraints are consistent, testable, and reusable. Remember that while scopes make code cleaner, they must still be supported by proper database indexing and should not replace dedicated service-layer logic for complex orchestration.

Up next: We will tackle Distributed Locks to prevent race conditions when multiple workers attempt to process the same invoice or subscription update simultaneously.

Similar Posts