Scopes (Local / Global)
Defining query scopes for reuse
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