Scopes (Local / Global)

Defining query scopes for reuse

graph TD A[Model Query] --> B{Has Global Scope?} B -->|Yes| C[Apply Global Scope] B -->|No| D[Continue] C --> D D --> E{Has Local Scope?} E -->|Yes| F[Apply Local Scope] E -->|No| G[Execute Query] F --> G H[Local Scope] -->|Explicit Call| E I[Global Scope] -->|Auto Apply| B

Scopes allow you to define common queries as reusable methods. This pattern makes code cleaner and more maintainable, preventing repetition of similar queries.


Types of Scopes:


1. Local Scopes: Local scopes that must be explicitly called and are defined with scope prefix. These scopes are optional and only applied when explicitly called.


2. Global Scopes: Global scopes that are automatically applied to all model queries. These scopes are defined in the booted() method and used for mandatory filters like soft deletes or multi-tenancy.


Benefits of Using Scopes:


  • <strong>Reusability</strong>: Reusing query logic in different places
  • <strong>Readability</strong>: More readable and understandable code
  • <strong>Maintainability</strong>: Changes applied in one place
  • <strong>Testability</strong>: Testing query logic separately
  • <strong>Chainability</strong>: Ability to chain scopes for complex queries

Key Differences:


  • Local scopes are optional and must be explicitly called
  • Global scopes are automatically applied
  • Global scope can be disabled with <code>withoutGlobalScope()</code>
  • Local scopes can accept parameters
  • Global scopes are usually used for business rules

Examples

Simple Local Scope

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    public function scopeActive($query)
    {
        return $query->where('active', 1);
    }
    
    public function scopeVerified($query)
    {
        return $query->whereNotNull('email_verified_at');
    }
}

// Usage
$activeUsers = User::active()->get();
$verifiedActiveUsers = User::active()->verified()->get();

Defining and using simple local scopes that can be chained.

Local Scope with Parameters

<?php

class Post extends Model
{
    public function scopePublished($query)
    {
        return $query->where('status', 'published');
    }
    
    public function scopeByAuthor($query, $authorId)
    {
        return $query->where('author_id', $authorId);
    }
    
    public function scopeInDateRange($query, $startDate, $endDate)
    {
        return $query->whereBetween('created_at', [$startDate, $endDate]);
    }
}

// Usage
$posts = Post::published()
    ->byAuthor(1)
    ->inDateRange('2024-01-01', '2024-12-31')
    ->get();

Local scopes can accept parameters and be used for complex queries.

Global Scope

<?php

class User extends Model
{
    protected static function booted()
    {
        static::addGlobalScope('active', function ($query) {
            $query->where('active', 1);
        });
    }
}

// Automatically applied
$users = User::all(); // Only active users

// Disable global scope
$allUsers = User::withoutGlobalScope('active')->get();

// Disable all global scopes
$allUsers = User::withoutGlobalScopes()->get();

Global scope that is automatically applied and can be disabled.

Global Scope Class

<?php

namespace App\Models\Scopes;

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

class ActiveScope implements Scope
{
    public function apply(Builder $builder, Model $model)
    {
        $builder->where('active', 1);
    }
}

// In Model
class User extends Model
{
    protected static function booted()
    {
        static::addGlobalScope(new ActiveScope);
    }
}

Creating global scope as a class for more complex logic and better testability.

Dynamic Scopes

<?php

class Product extends Model
{
    public function scopeOfType($query, $type)
    {
        return $query->where('type', $type);
    }
    
    public function scopePriceRange($query, $min, $max)
    {
        return $query->whereBetween('price', [$min, $max]);
    }
    
    public function scopeInStock($query)
    {
        return $query->where('stock', '>', 0);
    }
}

// Dynamic query building
$query = Product::query();

if ($request->has('type')) {
    $query->ofType($request->type);
}

if ($request->has('min_price') && $request->has('max_price')) {
    $query->priceRange($request->min_price, $request->max_price);
}

$query->inStock();

$products = $query->get();

Using scopes to build dynamic queries based on conditions.

Anonymizing Global Scope

<?php

class Tenant extends Model
{
    protected static function booted()
    {
        static::addGlobalScope('tenant', function ($query) {
            $query->where('tenant_id', auth()->user()->tenant_id);
        });
    }
}

// All queries automatically filtered by tenant
$items = Item::all(); // Only current tenant's items

// Access other tenant's data (with proper authorization)
$allItems = Item::withoutGlobalScope('tenant')->get();

Using global scope for multi-tenancy and automatic data filtering.

Scope in Relationship

<?php

class User extends Model
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
    
    public function publishedPosts()
    {
        return $this->hasMany(Post::class)->published();
    }
}

// Usage
$user = User::find(1);
$allPosts = $user->posts; // All posts
$publishedPosts = $user->publishedPosts; // Only published posts

Using scope in relationship definition to filter results.

Use Cases

  • Filtering common queries like active users or published posts
  • Multi-tenancy and filtering data by tenant
  • Soft deletes and automatic filtering of deleted records
  • Dynamic query building based on user input
  • Reusable query logic across application
  • Business rules enforcement with global scopes

Common Mistakes

  • Using global scope for optional filters that should be disableable
  • Forgetting 'scope' prefix in local scope method name
  • Using global scope without ability to disable causing testing issues
  • Defining scope with complex logic that should be in service layer
  • Using scope for business logic that should be in model events
  • Forgetting return statement in scope causing errors

Best Practices

  • Use local scopes for optional queries
  • Use global scopes only for mandatory business rules
  • Always consider ability to disable global scope
  • Keep scopes in model class not in service classes
  • Use scopes for query logic not business logic
  • Document scopes if they have complex logic
  • Use dynamic scopes for query building

Edge Cases

  • Global scope with circular dependencies
  • Scope in relationship that may affect eager loading
  • Disabling global scope in testing
  • Scope with conditional logic that may affect performance
  • Global scope in queue jobs with different context
  • Scope with raw queries that may have SQL injection

Performance Notes

  • Global scopes overhead is very low
  • Local scopes have no negative impact on performance
  • Using scope for filtering can optimize query
  • Scope with eager loading can solve N+1 problem
  • Use scope for selecting specific columns
  • Scope with indexes can improve performance

Security Notes

  • Ensure scopes properly escape user input
  • Use raw queries in scope with caution
  • Ensure global scope is not used for authorization
  • Use scope for sensitive data filtering
  • Ensure scopes don't have SQL injection

Interview Points

  • What is the difference between local scope and global scope?
  • How can you disable global scope?
  • When should you use local scope and when global scope?
  • How can you use scope in relationship?
  • What are the benefits and drawbacks of using scopes?
  • How can you define scope with parameters?

Version Notes

  • Laravel 11.x: Improved performance in scope resolution
  • Laravel 11.x: Better support for dynamic scopes
  • Laravel 10.x: Improved global scope disabling
  • Laravel 9.x: Improved scope chaining