Laravel Eloquent Query Scopes: Practical Deep Dive | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    Eloquent Query Scopes: Global, Local, and Dynamic Scopes Without the Magic Tax        On this page       1. [  Eloquent Query Scopes: Global, Local, and Dynamic Scopes Without the Magic Tax ](#eloquent-query-scopes-global-local-and-dynamic-scopes-without-the-magic-tax)
2. [  Global Scopes: Powerful but Dangerous ](#global-scopes-powerful-but-dangerous)
3. [  Local Scopes: The Workhorse ](#local-scopes-the-workhorse)
4. [  Dynamic Scopes via Dedicated Classes ](#dynamic-scopes-via-dedicated-classes)
5. [  Testing Scopes in Isolation with Pest ](#testing-scopes-in-isolation-with-pest)
6. [  Key Takeaways ](#key-takeaways)

  ![Eloquent Query Scopes: Global, Local, and Dynamic Scopes Without the Magic Tax](https://cdn.msaied.com/362/ecd807763e4e5019ee04875ba59dc8bc.png)

  #laravel   #eloquent   #database   #php  

 Eloquent Query Scopes: Global, Local, and Dynamic Scopes Without the Magic Tax 
================================================================================

     4 Jul 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   Eloquent Query Scopes: Global, Local, and Dynamic Scopes Without the Magic Tax  ](#eloquent-query-scopes-global-local-and-dynamic-scopes-without-the-magic-tax)
2. [  02   Global Scopes: Powerful but Dangerous  ](#global-scopes-powerful-but-dangerous)
3. [  03   Local Scopes: The Workhorse  ](#local-scopes-the-workhorse)
4. [  04   Dynamic Scopes via Dedicated Classes  ](#dynamic-scopes-via-dedicated-classes)
5. [  05   Testing Scopes in Isolation with Pest  ](#testing-scopes-in-isolation-with-pest)
6. [  06   Key Takeaways  ](#key-takeaways)

 Eloquent Query Scopes: Global, Local, and Dynamic Scopes Without the Magic Tax
------------------------------------------------------------------------------

Query scopes are deceptively simple. You add `scopeActive` to a model, call `->active()` on a query, and everything works. Then six months later a colleague spends two hours debugging why a count query returns zero — because a global scope silently filtered it out.

This article is about writing scopes that are powerful *and* honest: easy to discover, easy to test, and easy to remove when they're wrong.

---

### Global Scopes: Powerful but Dangerous

A global scope applies to **every** query on a model. Laravel's own `SoftDeletes` trait is the canonical example. The problem is that global scopes are invisible at the call site.

```php
// app/Models/Scopes/PublishedScope.php
use Illuminate\Database\Eloquent\{Builder, Model, Scope};

final class PublishedScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->whereNotNull('published_at')
                ->where('published_at', 'whereNotNull('published_at')
                 ->where('published_at', '=', now()->subDays($days));
}

```

Chaining reads naturally:

```php
$articles = Article::published()
    ->byAuthor($user->id)
    ->recent(7)
    ->orderByDesc('published_at')
    ->cursorPaginate(20);

```

Return `Builder` explicitly — it enables static analysis tools like PHPStan and Larastan to follow the chain.

---

### Dynamic Scopes via Dedicated Classes

When scope logic grows — conditional filters, multiple parameters, reuse across models — extract it into a dedicated invokable class:

```php
// app/Queries/ArticleFilters.php
final class ArticleFilters
{
    public function __construct(
        private readonly ?string $search,
        private readonly ?string $status,
        private readonly ?int $authorId,
    ) {}

    public function __invoke(Builder $query): Builder
    {
        return $query
            ->when($this->search, fn ($q, $s) =>
                $q->whereFullText(['title', 'body'], $s)
            )
            ->when($this->status === 'published', fn ($q) =>
                $q->published()
            )
            ->when($this->authorId, fn ($q, $id) =>
                $q->byAuthor($id)
            );
    }
}

```

```php
$filters = new ArticleFilters(
    search: $request->search,
    status: $request->status,
    authorId: $request->integer('author_id') ?: null,
);

$articles = Article::tap($filters)->cursorPaginate(20);

```

`tap()` passes the builder to any callable — no trait, no magic method needed.

---

### Testing Scopes in Isolation with Pest

```php
it('published scope excludes future articles', function () {
    Article::factory()->create(['published_at' => now()->addDay()]);
    Article::factory()->create(['published_at' => now()->subHour()]);

    expect(Article::published()->count())->toBe(1);
});

it('byAuthor scope filters correctly', function () {
    $author = User::factory()->create();
    Article::factory(3)->for($author, 'author')->published()->create();
    Article::factory(2)->published()->create(); // different author

    expect(Article::published()->byAuthor($author->id)->count())->toBe(3);
});

```

Test each scope independently before testing combinations. This isolates failures and keeps tests fast.

---

### Key Takeaways

- **Global scopes** are for invariants (soft deletes, tenant isolation), not business rules.
- **Local scopes** should return `Builder` explicitly for static analysis compatibility.
- **Invokable filter classes** with `tap()` replace bloated scope lists on large models.
- Always test scopes in isolation — one scope per test, then compose.
- Use `withoutGlobalScope(ClassName::class)` over `withoutGlobalScopes()` to be precise about what you're removing.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Feloquent-query-scopes-global-local-and-dynamic-scopes-without-the-magic-tax&text=Eloquent+Query+Scopes%3A+Global%2C+Local%2C+and+Dynamic+Scopes+Without+the+Magic+Tax) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Feloquent-query-scopes-global-local-and-dynamic-scopes-without-the-magic-tax) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  When should I use a global scope instead of a local scope?        Use a global scope only when every query on the model must respect the constraint without exception — soft deletes and tenant isolation are the classic cases. For anything that varies by context (admin vs. public, draft vs. published), a local scope called explicitly is safer and more discoverable. 

      Q02  How do I apply multiple optional filters without a long chain of `when()` calls on the model?        Extract the filters into an invokable class and pass it to the query builder via `tap()`. This keeps the model clean, makes the filter logic independently testable, and lets you type-hint constructor arguments for clarity. 

      Q03  Does returning Builder from a local scope break IDE autocompletion?        No — returning the concrete `Illuminate\Database\Eloquent\Builder` type (or the generic `Builder&lt;static&gt;` with a PHPDoc) is exactly what Larastan and modern IDEs expect. It enables full chain completion and catches type errors at analysis time rather than runtime. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![PostgreSQL CTEs, Window Functions, and Lateral Joins in Laravel](https://cdn.msaied.com/363/8e5685c14467b502c2bcbd62b4f47f64.png) laravel postgresql query-builder 

### PostgreSQL CTEs, Window Functions, and Lateral Joins in Laravel

Go beyond basic Eloquent queries: learn how to harness PostgreSQL CTEs, window functions, and LATERAL joins di...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 4 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/postgresql-ctes-window-functions-and-lateral-joins-in-laravel-2) [ ![FrankenPHP, OPcache JIT, and Preloading: Squeezing Real Throughput from Laravel](https://cdn.msaied.com/361/fc51b795acf24849e543d4f941b850a2.png) laravel frankenphp php 

### FrankenPHP, OPcache JIT, and Preloading: Squeezing Real Throughput from Laravel

A practical guide to running Laravel under FrankenPHP with OPcache JIT and preloading enabled — covering worke...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 4 Jul 2026     1 min read  

  Read    

 ](https://msaied.com/articles/frankenphp-opcache-jit-and-preloading-squeezing-real-throughput-from-laravel-1) [ ![Filament v4 Schema-Based Forms, Infolists, and the Unified Schema API](https://cdn.msaied.com/360/11c59afcba98933101f65c6357f5cd1c.png) filament laravel filament-v4 

### Filament v4 Schema-Based Forms, Infolists, and the Unified Schema API

Filament v4 replaces scattered form and infolist definitions with a single Schema API. Learn how the unified c...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 4 Jul 2026     3 min read  

  Read    

 ](https://msaied.com/articles/filament-v4-schema-based-forms-infolists-and-the-unified-schema-api-2) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
