Laravel's UseEloquentBuilder Attribute: Clean Custom Query Builders

Custom Eloquent query builders in Laravel used to require overriding the newEloquentBuilder
method on every model. The new UseEloquentBuilder
attribute makes this relationship explicit and eliminates boilerplate code.
The Old Override Pattern vs. The New Attribute Approach
Previously, registering a custom query builder meant adding a method override to your model, which often felt disconnected from the actual builder class:
class MyModel extends Model
{
// ...
public function newEloquentBuilder($query)
{
return new CustomBuilder($query);
}
}
The new UseEloquentBuilder
attribute makes this relationship clear and declarative:
use Illuminate\Database\Eloquent\Attributes\UseEloquentBuilder;
#[UseEloquentBuilder(CustomBuilder::class)]
class MyModel extends Model
{
// ...
}
This approach immediately tells developers which custom builder handles queries for this model, improving code readability and maintainability.
Real-World Example
Consider a blog platform where posts need complex filtering and search capabilities. You might create a specialized query builder to handle these operations:
<?php
namespace App\Builders;
use Illuminate\Database\Eloquent\Builder;
class PostBuilder extends Builder
{
public function published(): static
{
return $this->where('status', 'published')
->where('published_at', '<=', now());
}
public function byCategory(string $category): static
{
return $this->whereHas('categories', function ($query) use ($category) {
$query->where('slug', $category);
});
}
public function searchContent(string $term): static
{
return $this->where(function ($query) use ($term) {
$query->where('title', 'like', "%{$term}%")
->orWhere('excerpt', 'like', "%{$term}%")
->orWhere('content', 'like', "%{$term}%");
});
}
public function withEngagementMetrics(): static
{
return $this->withCount(['comments', 'likes'])
->withAvg('ratings', 'score');
}
public function trending(int $days = 7): static
{
return $this->published()
->withEngagementMetrics()
->where('created_at', '>=', now()->subDays($days))
->orderByDesc('likes_count')
->orderByDesc('comments_count');
}
}
use App\Builders\PostBuilder;
use Illuminate\Database\Eloquent\Attributes\UseEloquentBuilder;
#[UseEloquentBuilder(PostBuilder::class)]
class Post extends Model
{
protected $fillable = [
'title', 'slug', 'excerpt', 'content', 'status', 'published_at'
];
protected $casts = [
'published_at' => 'datetime',
];
public function categories()
{
return $this->belongsToMany(Category::class);
}
public function comments()
{
return $this->hasMany(Comment::class);
}
public function likes()
{
return $this->hasMany(Like::class);
}
public function ratings()
{
return $this->hasMany(Rating::class);
}
}
class BlogController extends Controller
{
public function index(Request $request)
{
$posts = Post::published()
->when($request->category, fn($q, $cat) => $q->byCategory($cat))
->when($request->search, fn($q, $term) => $q->searchContent($term))
->withEngagementMetrics()
->latest('published_at')
->paginate(12);
return view('blog.index', compact('posts'));
}
public function trending()
{
$trendingPosts = Post::trending(14)
->limit(10)
->get();
return view('blog.trending', compact('trendingPosts'));
}
}
The attribute approach makes it immediately obvious that the Post
model uses a specialized builder with custom query methods. This is particularly valuable in large applications where multiple models might use custom builders, as it eliminates the need to dig into method implementations to understand the query capabilities.
This pattern also improves IDE support and static analysis, as the relationship between model and builder is explicitly declared at the class level. Team members can quickly identify which models have enhanced query capabilities without examining the entire class implementation.
Stay Updated with More Laravel Tips
Enjoyed this article? There's plenty more where that came from! Subscribe to our channels to stay updated with the latest Laravel tips, tricks, and best practices: