Hello! Let's dive into the next lesson in our "Advanced Eloquent ORM" module.
Introduction
In our last lesson, we focused on making our individual model instances more intelligent by using accessors, mutators, and attribute casting. We learned how to transform data as it enters and leaves our application, ensuring it's always in the right format. For example, we made sure product names are always capitalized correctly before saving and prices are formatted nicely for display.
This lesson elevates that concept from a single model to entire collections of models. Instead of transforming a single attribute, we're going to learn how to apply reusable constraints to our database queries.
Our goal is to implement local and global query scopes to create reusable query constraints. You'll discover how to stop repeating where clauses and encapsulate common query logic into clean, readable methods. By the end of this lesson, you'll be able to:
- Define and apply local scopes for common, optional query filters.
- Implement global scopes for constraints that should apply to every query on a model by default.
- Write cleaner, more expressive, and maintainable query code.
This is a fundamental technique for building clean, large-scale Laravel applications.
1. Local Query Scopes: Reusable Filters
Imagine you frequently need to retrieve only the "active" products from your database. You might find yourself writing this code in multiple places:
// In a controller
$activeProducts = Product::where('is_active', true)->get();
// In an API resource
$featuredActiveProducts = Product::where('is_featured', true)->where('is_active', true)->get();
// In a command
$productsToProcess = Product::where('is_active', true)->where('stock', '>', 0)->get();
Notice the repetition of ->where('is_active', true). If the definition of "active" ever changes (perhaps requiring a published_at date to be in the past), you would have to find and update every instance. This violates the DRY (Don't Repeat Yourself) principle.
Local query scopes solve this by allowing you to extract this logic into a reusable method on your Eloquent model.
Understanding Query Scopes — 30 Days of Laravel: Day 5
This video from Mateus Guimarães provides an excellent introduction to local query scopes. It clearly demonstrates the problem of repetitive where clauses and shows how to solve it by creating simple, dynamic, and chainable scope methods.
Watch the video from the beginning to 04:22. Pay attention to: How a scope is defined with the scope prefix (e.g., scopeHasUrl). How it's used without the prefix (e.g., ->hasUrl()). How you can pass parameters to create dynamic scopes. How scopes can be chained with other Eloquent methods.
As the video demonstrated, a local scope is simply a method in your model prefixed with scope. Laravel automatically recognizes this convention and allows you to call the method on a query builder without the scope prefix.
Your Turn: Implementing Local Scopes
Let's apply this to our Product model.
1. Create an active scope:
In the previous lesson, we added an is_active boolean column to our products table. This is a perfect use case for a scope.
Open your app/Models/Product.php model and add the following method:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
use HasFactory;
// ... your casts and other properties ...
/**
* Scope a query to only include active products.
*/
public function scopeActive(Builder $query): void
{
$query->where('is_active', true);
}
}
Now, instead of Product::where('is_active', true)->get(), you can simply write:
$activeProducts = Product::active()->get();
This is not only shorter but also much more descriptive. It reads like plain English.
2. Create a dynamic ofCategory scope:
Let's create a scope that filters products belonging to a specific category. This scope will need to accept a parameter: the category ID.
Add another method to your Product.php model:
// In app/Models/Product.php, after the active() scope method
/**
* Scope a query to only include products of a given category.
*/
public function scopeOfCategory(Builder $query, int $categoryId): void
{
$query->where('category_id', $categoryId);
}
Now you can easily find all active products in category 1:
$products = Product::active()->ofCategory(1)->get();
See how you can chain scopes together? This is what makes them so powerful for building complex but readable queries.
2. Global Query Scopes: Automatic Constraints
While local scopes are for optional filters, global scopes are for constraints that should apply to all queries for a given model by default. This is extremely useful for things like multi-tenancy, where you must ensure a user can only see data belonging to their team or organization.
Laravel's own Soft Deleting feature is a prime example. When you use the SoftDeletes trait, a global scope is automatically added to exclude any records where deleted_at is not null.
There are two main ways to define a global scope: as a dedicated class or as an anonymous closure.
Learn to master Query Scopes in Laravel
This article from Laravel News provides a fantastic, in-depth look at both local and global scopes. We'll focus on the global scope sections to understand how to create and apply them.
Read the following sections: Global Query Scopes: Understand the core concept and the multi-tenancy example. How to Create Global Query Scopes: Focus on creating a dedicated Scope class using the Artisan command. Applying Global Query Scopes: See the two ways to apply the scope to your model: the #[ScopedBy] attribute and the booted() method.
Reusable Global Scopes with Parameters
Just like local scopes, global scopes can be made more flexible. By designing them to accept parameters in their constructor, you can reuse the same scope class across different models with different rules.
Laravel Global Scopes with Parameters for Global Usage
The Laravel Daily channel shows a brilliant practical example: a single OrderScope class that is used to apply different default ordering to multiple models (Lesson and Course). This highlights how to create flexible, reusable global scopes.
Watch this short video (00:00 - 02:29) to see how a parameterized constructor allows a single scope class to handle ordering by different columns and directions across various models.
Ignoring Global Scopes
Of course, sometimes you need to bypass a global scope. For instance, an admin might need to see all records, including soft-deleted ones. Eloquent provides simple methods to temporarily disable global scopes for a specific query.
Learn to master Query Scopes in Laravel
Let's return to the Laravel News article to see how to remove these automatic constraints when needed.
Read the sections Anonymous Global Query Scopes and Ignoring Global Query Scopes. This will show you a simpler way to define scopes for one-off cases and, crucially, how to use withoutGlobalScope() and withoutGlobalScopes().
Your Turn: Implementing a Global Scope
For our application, let's say we want products to always be listed in alphabetical order by default. This is a great candidate for a simple, anonymous global scope.
Open your app/Models/Product.php model and add the booted() method:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
// ... other use statements
class Product extends Model
{
// ...
/**
* The "booted" method of the model.
*/
protected static function booted(): void
{
static::addGlobalScope('alphabetical', function (Builder $builder) {
$builder->orderBy('name', 'asc');
});
}
// ... your scope methods ...
}
That's it! Now, any time you run a query like Product::all() or Product::active()->get(), Laravel will automatically add ORDER BY name ASC to the generated SQL.
To test ignoring it, you can run this in php artisan tinker:
// This will be ordered by name ASC
\App\Models\Product::all();
// This will use the database's default order (likely by primary key)
\App\Models\Product::withoutGlobalScope('alphabetical')->get();
It's important to remember a key "gotcha" mentioned in the article: global scopes are only applied to Eloquent queries. They will not be applied if you use the DB facade, like DB::table('products')->get().
Conclusion
You've now added a powerful tool to your Eloquent toolkit. By encapsulating query logic into scopes, you make your code more readable, maintainable, and less prone to errors.
Key Takeaways:
- Local Scopes are manually applied filters for common, optional constraints (
->active()). They are defined asscopeMyScope()methods on the model. - Global Scopes are constraints applied automatically to all model queries (
where('team_id', ...)). They are ideal for security and application-wide business rules. - Scopes can be dynamic, accepting parameters to make them more flexible (
->ofType('admin')). - Global scopes can be temporarily disabled for a query using
withoutGlobalScope()orwithoutGlobalScopes().
Next Up:
We've explored how to transform attributes and constrain queries. In the next lesson, we will venture into more complex data structures by learning how to define and query polymorphic relationships. This will allow you to build flexible systems where a model can belong to more than one other type of model, such as allowing both blog posts and videos to have comments.