Laravel Eloquent Query Scopes: Practical Deep Dive | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://msaied.com) - [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [About](https://msaied.com#about)

           [  Contact](https://msaied.com#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://msaied.com)
- [ProjectsCase studies](https://msaied.com/projects)
- [ArticlesEngineering notes](https://msaied.com/articles)
- [CertificatesCredentials](https://msaied.com/certificates)
- [AboutHow I work](https://msaied.com#about)
- [ContactGet in touch](https://msaied.com#contact)

  [Start a conversation](https://msaied.com#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://msaied.com)
2. /
3. [Articles](https://msaied.com/articles)
4. /
5. Eloquent Query Scopes: Global, Local, and Dynamic Scopes Without the Magic Tax

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

 Query scopes are one of Eloquent's most misused features. This guide shows how to write global, local, and dynamic scopes that stay testable, composable, and free of hidden side-effects.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://msaied.com#person) Published 4 Jul 2026 · Updated 4 Jul 2026 · 3 min read

ShareCopy linkCopied

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

  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
------------------------------------------------------------------------------

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.

- [laravel](https://msaied.com/articles?search=laravel)
- [eloquent](https://msaied.com/articles?search=eloquent)
- [database](https://msaied.com/articles?search=database)
- [php](https://msaied.com/articles?search=php)

 Frequently asked questions 
---------------------------

  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.

   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.

   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.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

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

[About](https://msaied.com#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleFrankenPHP, OPcache JIT, and Preloading: Squeezing Real Throughput from Laravel](https://msaied.com/articles/frankenphp-opcache-jit-and-preloading-squeezing-real-throughput-from-laravel-1) [Next articlePostgreSQL CTEs, Window Functions, and Lateral Joins in Laravel](https://msaied.com/articles/postgresql-ctes-window-functions-and-lateral-joins-in-laravel-2)  

   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)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://msaied.com#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/731/7cabd03b86a18f1db2c9ff7de1510270.png)  · 3 min read### Laravel Octane + FrankenPHP: Request Lifecycle, Shared State, and Safe Singleton Patterns

3 Oct 2026 ](https://msaied.com/articles/laravel-octane-frankenphp-request-lifecycle-shared-state-and-safe-singleton-patterns) [ ![](https://cdn.msaied.com/728/b96439f5ef5f084b44d736951875f964.png)  · 3 min read### Eloquent Custom Relations: Polymorphic Pivots, Has-One-Of-Many, and Raw Join Relations

2 Oct 2026 ](https://msaied.com/articles/eloquent-custom-relations-polymorphic-pivots-has-one-of-many-and-raw-join-relations) [ ![](https://cdn.msaied.com/727/7873d7495db459e7a6e3bfbb8835852d.png)  · 3 min read### Laravel Caching Strategies: Tags, Stampede Prevention, and Cache-Aside at Scale

2 Oct 2026 ](https://msaied.com/articles/laravel-caching-strategies-tags-stampede-prevention-and-cache-aside-at-scale-2) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://msaied.com#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://msaied.com)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://msaied.com/sitemap.xml)
