Never Write That WHERE Clause Again: How Anonymous Global Scopes Work in Eloquent

How Anonymous Global Scopes Work in Eloquent

How Anonymous Global Scopes Work in Eloquent


In almost every Laravel application, there are certain database query constraints that you find yourself writing again and again. Whether it is filtering out inactive users, hiding draft blog posts, or scoping multi-tenant records by a tenant_id, repeating ->where('is_active', true) across dozens of controllers and services is tedious and error-prone.

If you forget that WHERE clause even once, you risk displaying unpublished content or exposing sensitive data across tenants.

While Laravel provides full Global Scope classes, you don't always need to create a dedicated class file for simple filters. In this tutorial, we will learn how to use Anonymous Global Scopes in Eloquent, how they work under the hood, how to disable them when needed, and best practices to follow.

What are Anonymous Global Scopes?

A global scope automatically adds constraints to all queries executed on a given Eloquent model. Laravel supports two types of global scopes:

  1. Dedicated Scope Classes: Standalone classes implementing the Illuminate\Database\Eloquent\Scope interface.
  2. Anonymous Global Scopes: Inline closures attached inside the model's booted() method using the addGlobalScope() method.

Anonymous global scopes are convenient because they allow you to define query constraints directly inside your model file without creating separate class files in app/Models/Scopes.

Basic Syntax

You can register an anonymous global scope inside the model's booted() method using static::addGlobalScope():


namespace App\Models;

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

class Post extends Model
{
    protected static function booted(): void
    {
        static::addGlobalScope('published', function (Builder $builder) {
            $builder->where('status', 'published');
        });
    }
}


Let's understand what is happening here:

  • The first argument, 'published', is a unique string identifier name for the scope. This allows you to reference or remove it later.
  • The second argument is a closure that receives an instance of the Eloquent Builder, where you can attach any standard query constraint.

How It Works in Practice

Once registered, any query executed against the Post model automatically includes the constraint.

For example, when you run:

$posts = Post::all();


Laravel behind the scenes executes the following SQL query:

SELECT * FROM `posts` WHERE `status` = 'published';


Even if you perform relationship queries or complex joins, the scope will automatically be applied:

$user = User::with('posts')->find(1);


The loaded posts relation will automatically contain only records with status = 'published'.

Real-World Use Cases

1. Multi-Tenant Application Scoping

If you build a multi-tenant application where users belong to a specific company or team, you can scope all queries to the authenticated tenant automatically:

namespace App\Models;

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

class Invoice extends Model
{
    protected static function booted(): void
    {
        static::addGlobalScope('tenant', function (Builder $builder) {
            if (auth()->check() && auth()->user()->tenant_id) {
                $builder->where('tenant_id', auth()->user()->tenant_id);
            }
        });
    }
}


2. Active Users Filter

To avoid querying deactivated or banned accounts across your authentication or user directories:

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    protected static function booted(): void
    {
        static::addGlobalScope('active', function (Builder $builder) {
            $builder->where('is_active', true);
        });
    }
}


How to Bypass or Remove Global Scopes

There are times when you need to query the database without the global scope—such as in an admin panel where you want to view all posts (including drafts) or manage deactivated users.

Laravel provides two methods to remove global scopes from specific queries:

1. Removing a Specific Global Scope with withoutGlobalScope()

Pass the string identifier you defined in addGlobalScope() to the withoutGlobalScope() method:

// Fetch all posts including drafts
$allPosts = Post::withoutGlobalScope('published')->get();

// Find a single post even if it is a draft
$draftPost = Post::withoutGlobalScope('published')->findOrFail($postId);


2. Removing All Global Scopes with withoutGlobalScopes()

If your model has multiple global scopes applied and you want to remove all of them (or a subset), use withoutGlobalScopes():

// Removes all global scopes applied to Post
$posts = Post::withoutGlobalScopes()->get();

// Removes only specified global scopes
$posts = Post::withoutGlobalScopes(['published', 'active'])->get();


Anonymous Global Scope vs Dedicated Scope Class

Feature Anonymous Global Scope Class-Based Global Scope
Definition Closure inside booted() method. Separate class in app/Models/Scopes.
Removal Syntax withoutGlobalScope('name') (string identifier). withoutGlobalScope(ScopeClass::class).
Reusability Best for single-model logic. Reusable across multiple models.

Common Mistakes to Avoid

  • Forgetting the scope name: While Laravel allows passing just a Closure to addGlobalScope($closure) without a string name, doing so makes it impossible to remove the scope individually via withoutGlobalScope(). Always provide a clear string name as the first argument.
  • Adding heavy queries or external network calls: The scope closure runs on every single query for that model. Never perform expensive database lookups or API calls inside a global scope closure.
  • Unexpected behavior in authentication: If you add an active scope on the User model, Auth::attempt() or Auth::user() queries will automatically filter out inactive users. Make sure this behavior aligns with your application requirements.

Conclusion:

Anonymous Global Scopes in Laravel Eloquent provide a clean and concise way to ensure mandatory query constraints are consistently enforced across your application. They save you from writing repetitive WHERE clauses and safeguard your data against accidental exposure.

Thank you for reading this article 😊

For any query, do not hesitate to comment 💬



Previous Post Next Post

Contact Form