Innovate | Connect | Transform
ContactService statusUSD $

GICT knowledge base

Laravel 12 Global Scope Guide – Easy Query Filtering

Published

Laravel

Laravel 12 has new global scope features that filter database queries automatically. These scopes add filtering to every query without you needing to do anything manually.

When you use global scopes properly, your app becomes more secure, consistent, and easier to maintain. This guide shows you how to use Laravel 12 global scopes.

What Are Global Scopes?

Global scopes are filters that run automatically on every database query for a model. When you set up a global scope for a model, Laravel adds the scope conditions to all queries for that model.

Global scopes are different from local scopes because they work automatically. Local scopes need you to call them manually. Global scopes just happen every time you query the database.

This makes global scopes great for security rules, data checks, and business logic that should always apply. You write the filter once, and it works everywhere in your app.

Global scopes catch every database query for a model and add WHERE clauses automatically. This means important filtering never gets forgotten.

Think about an app where users from different companies use the same system. Without global scopes, you would need to remember to filter by company in every controller and query. With global scopes, you set up the company filter once and it works everywhere.

Creating Basic Global Scopes

Laravel 12 gives you different ways to make global scopes. The easiest way is using an Artisan command to create a new scope class.

Make a new global scope with this command:

php artisan make:scope ActiveScope

This creates a scope class in app/Models/Scopes. The class uses the Scope interface and gives you the structure for your filtering.

The apply method is where you write your filtering logic. It gets the query builder and model as inputs. You can use any query builder methods to add your filters.

To use this global scope in your model, add it in the model’s booted method. After adding the global scope, every query on the Product model includes the active filter automatically. When you call Product::all() it only returns products where active is true.

Simple Global Scopes with Anonymous Functions

For simple filtering that doesn’t need a separate file, you can make anonymous global scopes using closures in your model.

Anonymous scopes work well for basic conditions like status checks, true/false filters, or simple date checks. They keep your code short and don’t need separate files when the filtering is simple.

You can combine multiple conditions in one anonymous scope too.

But anonymous scopes have limits compared to class-based scopes. They can’t take parameters, use dependency injection, or handle complex business logic that needs external services.

For basic filtering like active/inactive status, you can use anonymous functions directly in your model:

// Product.php
class Product extends Model
{
    protected static function booted()
    {
        static::addGlobalScope('active', function ($query) {
            $query->where('active', true);
        });
    }
}

// Usage
$products = Product::all(); // Only active products

Multi-Tenant Data Filtering

One of the best uses for global scopes is multi-tenancy. Multi-tenant apps serve multiple organizations while keeping their data separate.

This scope filters all queries to only show records for the current user’s tenant. It keeps data completely separate between different organizations using your app.

You can also make more advanced tenant detection logic. This approach checks multiple places for tenant identification, giving you flexibility in how tenants are found.

Multi-tenant apps need to separate data between different organizations. Here’s a simple tenant filter:

// Order.php
class Order extends Model
{
    protected static function booted()
    {
        static::addGlobalScope('tenant', function ($query) {
            if (auth()->check()) {
                $query->where('tenant_id', auth()->user()->tenant_id);
            }
        });
    }
}

// Usage
$orders = Order::all(); // Only current tenant's orders

Content Publishing and Visibility

Content management systems and blogs often need to control which content shows to different users. Global scopes work well for this.

This scope makes sure regular visitors only see published content while letting authorized users see unpublished drafts and scheduled content.

You can extend this idea to make more complex visibility rules.

For blogs or CMS, you want to show only published content to regular users but allow editors to see drafts:

// Article.php
class Article extends Model
{
    protected static function booted()
    {
        static::addGlobalScope('visibility', function ($query) {
            $user = auth()->user();
            
            if (!$user || !$user->can('view-drafts')) {
                // Regular users see only published content
                $query->where('status', 'published')
                      ->where('published_at', '<=', now());
            }
            // Editors can see all content including drafts
        });
    }
}

// Usage
$articles = Article::all(); // Published articles for users, all for editors
$allArticles = Article::withoutGlobalScope('visibility')->get(); // All articles

User Access Control and Ownership

Global scopes can handle access control by filtering records based on user permissions or ownership.

This makes sure users can only access records they own unless they have admin privileges.

For more complex situations, you can make team-based access control.

Users should only see their own data unless they’re admins:

// Document.php
class Document extends Model
{
    protected static function booted()
    {
        static::addGlobalScope('ownership', function ($query) {
            if (auth()->check() && !auth()->user()->is_admin) {
                $query->where('user_id', auth()->id());
            }
        });
    }
}

// Usage
$documents = Document::all(); // Only user's documents

Using Multiple Global Scopes

Many apps need multiple filtering criteria at the same time. Laravel lets you register multiple global scopes on one model, and they run in the order you register them.

Each scope adds its conditions to the query builder, creating a combined filtering effect. The order matters because later scopes build on the constraints added by earlier scopes.

When designing multiple scopes, make sure they work well together. Don’t create scopes that might conflict with each other or give unexpected results when combined.

You can also create compound scopes that handle multiple related conditions. Compound scopes can be more efficient than multiple separate scopes because they reduce method calls and can optimize the SQL.

You can combine multiple scopes on the same model for layered filtering:

// Product.php
class Product extends Model
{
    protected static function booted()
    {
        // Tenant filter
        static::addGlobalScope('tenant', function ($query) {
            if (auth()->check()) {
                $query->where('tenant_id', auth()->user()->tenant_id);
            }
        });
        
        // Active filter
        static::addGlobalScope('active', function ($query) {
            $query->where('active', true);
        });
    }
}

// Usage
$products = Product::all(); // Tenant + active products
$allProducts = Product::withoutGlobalScopes()->get(); // All products

Removing Global Scopes Temporarily

Sometimes you need to bypass global scopes for specific queries, like admin functions, data exports, or complex reports. Laravel gives you several methods to remove global scopes temporarily.

Remove a specific scope from one query:

Product::withoutGlobalScope(ActiveScope::class)->get();

Remove multiple specific scopes:

Product::withoutGlobalScopes([ActiveScope::class, TenantScope::class])->get();

Remove all global scopes from a query:

Product::withoutGlobalScopes()->get();

You can also remove anonymous scopes by their string name:

Product::withoutGlobalScope('active')->get();

Use these methods carefully, especially with security-related scopes. Always check that removing scopes doesn’t expose sensitive data or break business rules. Consider adding extra authorization checks when bypassing security scopes.

Database Performance Tips

Global scopes can affect database performance if not done thoughtfully. Good optimization makes sure your scopes run efficiently even with large datasets.

Create proper database indexes for columns used in global scopes. If your scope filters by an active column, create an index:

CREATE INDEX idx_products_active ON products(active);

For scopes that filter by multiple columns, consider composite indexes:

CREATE INDEX idx_products_tenant_active ON products(tenant_id, active);

The order of columns in composite indexes matters. Put the most selective column first, usually the one that filters out the most records.

Monitor query performance using Laravel’s built-in debugging tools. For apps with high query volumes, consider caching frequently accessed filtered data.

Testing Strategies

Testing global scopes needs careful attention to their automatic behavior. You need to check that scopes apply correctly in various situations and can be bypassed when necessary.

Test global scopes alone and combined with other query methods, relationships, and Laravel features to make sure they work correctly in all situations.

Best Practices and Patterns

Design your global scopes with clear, descriptive names that show their purpose:

  • ActiveRecordsScope
  • TenantIsolationScope
  • PublishedContentScope

Keep scope logic simple and focused on one responsibility. Each scope should handle one specific filtering concern rather than multiple unrelated conditions.

Document your global scopes well to help other developers understand their behavior.

Organize scope classes in a logical directory structure under app/Models/Scopes with subdirectories for different types of scopes.

Common Mistakes to Avoid

Don’t overuse global scopes in your application. Apply them only for truly global constraints that should affect every query for a model. Too many global scopes can make debugging difficult and hurt performance.

Don’t put complex business logic directly in global scopes. Keep scope logic simple and fast to prevent performance problems. Complex business rules belong in service classes, repository methods, or dedicated business logic layers.

Be very careful when removing global scopes in production code, especially security-related scopes. Always add proper authorization checks and validation when bypassing security constraints.

Don’t create global scopes that depend on external state that might not be available in all contexts, such as session data, request parameters, or user authentication. Design scopes to handle missing context gracefully.

Don’t forget to test edge cases where global scopes might behave unexpectedly, such as with complex joins, subqueries, raw SQL queries, or third-party package integrations.

Never assume that global scopes will solve all your filtering needs. Some situations need more flexible approaches like local scopes, query builders, or repository patterns.

Comparing with Other Approaches

Global scopes work best for security-critical filtering, universal business rules, and data integrity constraints. Use them when you need automatic, consistent filtering across your entire application without manual work.

Local scopes give more flexibility for optional filtering that users can choose to apply. They work well for context-specific conditions, user preferences, and complex queries that need customization based on different situations.

Query builders offer maximum control for one-time filtering needs, complex joins, aggregations, and ad-hoc reporting queries. They give complete flexibility but need manual implementation and careful attention to consistency.

Repository patterns can wrap filtering logic while keeping testability and separation of concerns. They work well for complex business logic that goes beyond simple database filtering and needs interaction with multiple services or data sources.

Service classes handle complex business rules and coordinate multiple operations that might involve filtering, validation, transformation, and integration with external services.

Choose the approach that best fits your specific use case, considering factors like security requirements, performance needs, code maintainability, and team expertise.

Conclusion

Global scopes in Laravel 12 offer a clean and powerful way to automatically apply query filters across your application. They improve consistency, security, and performance while reducing repetitive code.

Start by identifying common, security-critical filters—like multi-tenancy, access control, and visibility rules—and convert them into global scopes. Gradually extend to universal constraints such as active status or date-based filters, but avoid overusing them where flexibility is needed.

Always provide ways to bypass scopes for admin tasks or complex reports, and test thoroughly to ensure they behave correctly across all scenarios and integrations.

Used thoughtfully, global scopes are a key tool for building secure, maintainable, and efficient Laravel applications.

Related guides

Account services are unavailable until the production backend is configured.