Laravel Eloquent Scopes: Pitfalls &amp; Composition | 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)    Laravel Eloquent Global vs Local Scopes: Pitfalls, Ordering, and Scope Composition        On this page       1. [  Laravel Eloquent Global vs Local Scopes: Pitfalls, Ordering, and Scope Composition ](#laravel-eloquent-global-vs-local-scopes-pitfalls-ordering-and-scope-composition)
2. [  How Global Scopes Are Applied ](#how-global-scopes-are-applied)
3. [  The Soft-Delete Conflict ](#the-soft-delete-conflict)
4. [  Removing Global Scopes Selectively ](#removing-global-scopes-selectively)
5. [  Local Scopes and Composition ](#local-scopes-and-composition)
6. [  Scope Macros for Cross-Model Reuse ](#scope-macros-for-cross-model-reuse)
7. [  Testing Scopes in Isolation ](#testing-scopes-in-isolation)
8. [  Takeaways ](#takeaways)

  ![Laravel Eloquent Global vs Local Scopes: Pitfalls, Ordering, and Scope Composition](https://cdn.msaied.com/496/a70ec2727a0239a23590ef1107e6af75.png)

  #laravel   #eloquent   #database   #testing   #architecture  

 Laravel Eloquent Global vs Local Scopes: Pitfalls, Ordering, and Scope Composition 
====================================================================================

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

       Table of contents

1. [  01   Laravel Eloquent Global vs Local Scopes: Pitfalls, Ordering, and Scope Composition  ](#laravel-eloquent-global-vs-local-scopes-pitfalls-ordering-and-scope-composition)
2. [  02   How Global Scopes Are Applied  ](#how-global-scopes-are-applied)
3. [  03   The Soft-Delete Conflict  ](#the-soft-delete-conflict)
4. [  04   Removing Global Scopes Selectively  ](#removing-global-scopes-selectively)
5. [  05   Local Scopes and Composition  ](#local-scopes-and-composition)
6. [  06   Scope Macros for Cross-Model Reuse  ](#scope-macros-for-cross-model-reuse)
7. [  07   Testing Scopes in Isolation  ](#testing-scopes-in-isolation)
8. [  08   Takeaways  ](#takeaways)

 Laravel Eloquent Global vs Local Scopes: Pitfalls, Ordering, and Scope Composition
----------------------------------------------------------------------------------

Eloquent scopes are one of those features that feel obvious until a production bug teaches you otherwise. Global scopes silently modify every query on a model. Local scopes are chainable named constraints. Both are powerful — and both have sharp edges when composed at scale.

### How Global Scopes Are Applied

Global scopes are registered in `booted()` and injected into every query builder instance for that model. The order of registration matters because each scope appends its own `WHERE` clauses, and some scopes wrap the query in a subquery or add `JOIN`s that interact with later scopes.

```php
class ActiveScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where($model->getTable() . '.active', true);
    }
}

class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where(
            $model->getTable() . '.tenant_id',
            app(TenantContext::class)->id()
        );
    }
}

```

Always qualify column names with the table alias. Without it, a `JOIN` added by another scope will cause an ambiguous column error that only surfaces in specific query paths.

### The Soft-Delete Conflict

`SoftDeletes` registers its own global scope (`SoftDeletingScope`). If you add a custom global scope that also touches `deleted_at`, you risk double-wrapping conditions or accidentally shadowing the soft-delete filter when using `withTrashed()`.

```php
// Dangerous: your scope re-adds deleted_at logic
public function apply(Builder $builder, Model $model): void
{
    $builder->whereNull('deleted_at')->where('active', true);
}

```

The fix is to check whether the soft-delete scope is already applied:

```php
public function apply(Builder $builder, Model $model): void
{
    if (in_array(SoftDeletes::class, class_uses_recursive($model), true)) {
        // Let SoftDeletingScope handle deleted_at
        $builder->where('active', true);
    } else {
        $builder->whereNull('deleted_at')->where('active', true);
    }
}

```

### Removing Global Scopes Selectively

`withoutGlobalScope()` accepts either the class name or the string key used during registration. Forgetting this causes subtle bugs when you need admin queries to bypass tenant isolation.

```php
// Remove a single scope
Post::withoutGlobalScope(TenantScope::class)->get();

// Remove all global scopes
Post::withoutGlobalScopes()->get();

// Remove multiple specific scopes
Post::withoutGlobalScopes([TenantScope::class, ActiveScope::class])->get();

```

Document every global scope on the model with a `@uses` docblock so future engineers know what implicit filters exist.

### Local Scopes and Composition

Local scopes are clean for composable, named constraints. The pitfall is returning `void` instead of `Builder` — doing so breaks chaining silently in older PHP versions (PHP 8+ will surface the type error).

```php
public function scopePublished(Builder $query): Builder
{
    return $query->where('status', Status::Published);
}

public function scopeForCategory(Builder $query, int $categoryId): Builder
{
    return $query->where('category_id', $categoryId);
}

// Composing cleanly
$posts = Post::published()->forCategory(3)->latest()->get();

```

### Scope Macros for Cross-Model Reuse

When the same constraint appears on multiple models, resist copy-pasting. Register a macro on the query builder:

```php
// In a ServiceProvider
Builder::macro('activeOnly', function (): Builder {
    /** @var Builder $this */
    return $this->where($this->getModel()->getTable() . '.active', true);
});

// Usage on any model
User::activeOnly()->get();
Product::activeOnly()->get();

```

This avoids a trait-per-model approach while keeping the constraint in one place.

### Testing Scopes in Isolation

Test global scopes by asserting the raw SQL, not just the result set:

```php
it('applies tenant scope to all queries', function () {
    $tenantId = 42;
    app()->instance(TenantContext::class, new TenantContext($tenantId));

    $sql = Post::toBase()->toSql();

    expect($sql)->toContain('"tenant_id" = ?');
});

```

For local scopes, test the composed query and the returned collection separately to isolate scope logic from data fixtures.

### Takeaways

- Always qualify column names in global scopes to survive `JOIN`s from other scopes.
- Check for `SoftDeletes` before adding your own `deleted_at` conditions.
- Use `withoutGlobalScope(ClassName::class)` precisely — never `withoutGlobalScopes()` in production code unless you mean it.
- Return `Builder` explicitly from local scopes; `void` breaks chaining.
- Extract repeated constraints to a `Builder` macro rather than duplicating scope traits.
- Assert raw SQL in scope tests, not just result counts.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Flaravel-eloquent-global-vs-local-scopes-pitfalls-ordering-and-scope-composition&text=Laravel+Eloquent+Global+vs+Local+Scopes%3A+Pitfalls%2C+Ordering%2C+and+Scope+Composition) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Flaravel-eloquent-global-vs-local-scopes-pitfalls-ordering-and-scope-composition) 

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

  3 questions  

     Q01  Can two global scopes on the same model conflict with each other?        Yes. If both scopes add conditions on the same column, or if one adds a JOIN that makes a column in the other scope ambiguous, you will get SQL errors or silently wrong results. Always qualify column names with the table name and test scopes together, not just in isolation. 

      Q02  When should I use a global scope versus a local scope?        Use a global scope only when a constraint must apply to every query on a model without exception — tenant isolation and soft deletes are the canonical cases. Use a local scope for opt-in constraints that callers compose explicitly. Overusing global scopes makes queries unpredictable and harder to debug. 

      Q03  Does withoutGlobalScope affect eager-loaded relationships?        No. Calling withoutGlobalScope on the parent model does not propagate to eager-loaded relationships. Each relationship query boots its own model instance and re-applies all registered global scopes. You must call withoutGlobalScope inside the relationship closure if you need to bypass it there too. 

  Continue reading

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

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

 [ ![PhpStorm 2026.2 Released: Laravel Tool Window, PHP 8.5 Pipe Operator, and AI Agents](https://cdn.msaied.com/505/151a0bba66cc27064e090e69e55d7c92.png) PhpStorm JetBrains PHP 8.5 

### PhpStorm 2026.2 Released: Laravel Tool Window, PHP 8.5 Pipe Operator, and AI Agents

PhpStorm 2026.2 ships a dedicated Laravel tool window with Artisan, error logs, and Laravel Cloud tabs, plus P...

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

 3 Aug 2026     3 min read  

  Read    

 ](https://msaied.com/articles/phpstorm-20262-released-laravel-tool-window-php-85-pipe-operator-and-ai-agents) [ ![Laravel Doctor: Diagnose Your Laravel App With One Artisan Command](https://cdn.msaied.com/504/d72224689abc7b396bce187535008272.png) Laravel Artisan Health Checks 

### Laravel Doctor: Diagnose Your Laravel App With One Artisan Command

Laravel Doctor is a first-party package announced at Laracon US 2026 that adds an `artisan doctor` command to...

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

 3 Aug 2026     4 min read  

  Read    

 ](https://msaied.com/articles/laravel-doctor-diagnose-your-laravel-app-with-one-artisan-command) [ ![Livewire v4.3.5 Released: Fix for SFC Detection with PHP Attribute Array Arguments](https://cdn.msaied.com/503/9678ed8dbf5d7a6f4f19ca7694cf241b.png) Livewire Laravel PHP 

### Livewire v4.3.5 Released: Fix for SFC Detection with PHP Attribute Array Arguments

Livewire v4.3.5 ships a targeted bug fix for Single File Component (SFC) detection when PHP attributes contain...

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

 3 Aug 2026     2 min read  

  Read    

 ](https://msaied.com/articles/livewire-v435-released-fix-for-sfc-detection-with-php-attribute-array-arguments) 

   [  ![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)
