When working with Laravel, one of the most powerful tools at your disposal is Eloquent ORM. While basic queries with where(), orderBy(), and relationships cover 80% of real-world needs, advanced applications often demand reusable, clean, and scalable query logic.
This is where Custom Query Builders and Model Scopes come into play. They allow you to extract business logic from controllers and services, keep your queries expressive and improve maintainability.
Index
- Introduction
- Model Scopes
- Custom Query Builders
- Combining Scopes and Builders
- When to Use What
- Performance Optimization with Scopes
- Real-Life Case Study
- Key Takeaways
- FAQs
- Conclusion
1. Model Scopes
Scopes are predefined query filters that can be reused across your project.
Example: Active Users Scope
Imagine you have a User model where users can be active or inactive.
Instead of writing this repeatedly:
$activeUsers = User::where('status', 'active')->get();
You can create a scope inside User.php:
class User extends Model
{
// Local Scope
public function scopeActive($query)
{
return $query->where('status', 'active');
}
// Dynamic Scope Example
public function scopeStatus($query, $status)
{
return $query->where('status', $status);
}
}
Now you can call:
$activeUsers = User::active()->get();
$inactiveUsers = User::status('inactive')->get();
Cleaner, reusable, and expressive.
“I tend to identify the awkward spots in the framework fairly quickly too, because I'm using it so much, in a real-world setting. The community drives the development in a lot of ways, and I add my own insights here and there too.” - Taylor Otwell
2. Global Scopes
Sometimes you need automatic query conditions applied everywhere.
For example, in a multi-tenant SaaS app, every model should automatically filter data by tenant_id.
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class TenantScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$builder->where('tenant_id', auth()->user()->tenant_id);
}
}
Attach it to your model:
class Project extends Model
{
protected static function booted()
{
static::addGlobalScope(new TenantScope);
}
}
Now:
Project::all(); // always filtered by tenant_id automatically
3. Custom Query Builders
For more complex logic, you can create custom query builder classes.
Real-Time Example: Filtering Orders
Suppose you’re building an eCommerce dashboard where admins filter orders by status, date, or payment method.
Instead of writing multiple if/else queries in controllers, create a custom query builder.
Step 1: Create Custom Builder
namespace App\QueryBuilders;
use Illuminate\Database\Eloquent\Builder;
class OrderBuilder extends Builder
{
public function paid()
{
return $this->where('payment_status', 'paid');
}
public function status($status)
{
return $this->where('status', $status);
}
public function dateBetween($start, $end)
{
return $this->whereBetween('created_at', [$start, $end]);
}
}
Step 2: Attach to Model
use App\QueryBuilders\OrderBuilder;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
public function newEloquentBuilder($query)
{
return new OrderBuilder($query);
}
}
Step 3: Use It in Controllers
$orders = Order::query()
->paid()
->status('shipped')
->dateBetween('2025-09-01', '2025-09-10')
->get();
Clean, chainable, and highly reusable.
4. Combining Scopes and Builders
You can mix local scopes and custom builders for maximum power.
Example:
$recentPaidOrders = Order::paid()
->status('delivered')
->dateBetween(now()->subDays(7), now())->get();
Here, paid() and status() come from the custom builder.
5. When to Use What?
- Local Scopes -> For small, reusable conditions (active(), published()).
- G*lobal Scopes ->* For tenant filtering, soft deletes, always-on conditions.
- Custom Query Builders -> For complex, chainable business queries across models.
“If you have to spend effort looking at a fragment of code and figuring out what it's doing, then you should extract it into a function and name the function after the what” - Martin Fowler
6. Performance Optimization with Scopes
Scopes can also help optimize queries by avoiding N+1 queries.
Example: Counting Relationships
Instead of:
$users = User::all();
foreach ($users as $user) {
echo $user->posts->count(); // N+1 problem
}
Define a scope in User model:
class User extends Model
{
public function scopeWithPostsCount($query)
{
return $query->withCount('posts');
}
}
Now:
$users = User::withPostsCount()->get();
foreach ($users as $user) {
echo $user->posts_count;//Single optimized query
}
7. Key Takeaways
- Local Scopes > Best for small, reusable query filters (e.g., published, activeUsers)
- Global Scopes > Apply system-wide rules automatically (e.g., tenant_id in multi-tenant apps).
- Custom Query Builders > Perfect for complex, chainable business queries (e.g., filtering orders by date, status, payment).
- Dynamic Scopes > Accept parameters for flexible filtering (e.g., category($id)).
- Performance Scopes > Use withCount() and eager loading inside scopes to prevent N+1 queries.
- Readability & Maintainability > Controllers and services stay clean, queries become self-explanatory.
When your app grows, organizing query logic with scopes and builders is not optional - it’s essential for scalability and maintainability.
“I have always wished that my computer would be as easy to use as my telephone; my wish has come true because I can no longer figure out how to use my telephone.” - Bjarne Stroustrup
8. Real-Life Case Study
In one of our projects (a multi-tenant SaaS CRM), we:
- Used global scopes to automatically restrict all queries by tenant_id.
- Defined local scopes like scopeActiveLeads() and scopeConvertedLeads().
- Built a custom query builder for filtering leads by industry, lead_score, and source. This reduced controller complexity by 70% and made queries self-explanatory for new developers.
Result:
- Cleaner controllers
- Fewer bugs from repeated query conditions
- Faster API responses
9. FAQs
1. What are Model Scopes in Laravel?
Reusable query filters in models to keep your queries clean and DRY.
2. When should I use Custom Query Builders?
For complex, chainable queries that go beyond simple filters.
3. Can Scopes and Builders be combined?
Yes, combining them allows both simple and advanced query logic.
4. How do they improve performance?
They reduce repetitive queries and prevent N+1 issues with optimized loading.
10. Conclusion
Laravel’s Eloquent ORM isn’t just about where and get. With local scopes, global scopes, and custom query builders, you can build clean, DRY, and scalable query logic that reads like plain English.
Use Local Scopes for small reusable filters.
- Use Global Scopes for automatic, system-wide conditions.
- Use Custom Builders for complex, chainable business queries.
- Optimize performance with withCount(), eager loading, and scope-based prefetching.
About Author: Manoj is a Senior PHP Laravel Developer at AddWeb Solution , building secure, scalable web apps and REST APIs while sharing insights on clean, reusable backend code.
Top comments (0)