Appearance
Custom Query Builder
Introduction
A Custom Query Builder extends Laravel's Illuminate\Database\Eloquent\Builder, letting you add custom query methods to your models. This encapsulates complex query logic so code stays readable, maintainable, and reusable.
Why use a Custom Query Builder?
- Reuse: Keep complex query logic in one place and call it from many callers.
- Readability: Instead of a long chain of
whereconditions, call a clearly named method such asProduct::query()->hasNoBom(). - Maintainability: Query logic lives in a single class, so changes and extensions stay simple.
Implementation
Implementation has two main steps: create the Builder class, then register it on the Model.
1. Create the Custom Query Builder class
Create a new class that extends Illuminate\Database\Eloquent\Builder. Define your custom query methods there.
Example with ProductBuilder:
php
<?php
declare(strict_types=1);
namespace Modules\Masterdata\Product\Infrastructure\Builders;
use App\Builders\IWithStandardQuery;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Modules\Masterdata\Product\Infrastructure\Models\Product;
// ...
/**
* @method static ProductBuilder query()
*
* @extends Builder<Product>
*/
final class ProductBuilder extends Builder implements IWithStandardQuery
{
public function haveSpecs(): self
{
return $this->whereHas('spec');
}
public function withCategory(): self
{
return $this->with('productCategory');
}
public function hasBom(): self
{
return $this->whereHas('boms');
}
public function hasNoBom(): self
{
return $this->whereDoesntHave('boms');
}
public function standardQuery(?Request $request = null): self
{
$request = $request ?? request();
// ... (complex filtering logic from the request)
if ($request->boolean('has_no_bom')) {
$this->hasNoBom();
}
// ...
return $this;
}
}2. Register the Builder on the Model
On the corresponding model (for example, Product), override newEloquentBuilder() to return an instance of your custom builder.
php
<?php
declare(strict_types=1);
namespace Modules\Masterdata\Product\Infrastructure\Models;
use Illuminate\Database\Eloquent\Model;
use Modules\Masterdata\Product\Infrastructure\Builders\ProductBuilder;
/**
* @method static ProductBuilder query()
*/
final class Product extends Model
{
public function newEloquentBuilder($query): ProductBuilder
{
return new ProductBuilder($query);
}
// ...
}3. Add PHPDoc to the Model
So IDEs (such as PhpStorm) understand Product::query() and provide accurate type-hinting, add a PHPDoc block with a @method annotation:
php
/**
* @method static ProductBuilder query()
*/
final class Product extends Model
{
// ...
}Usage
After registration, call your custom methods directly from the model.
php
use Modules\Masterdata\Product\Infrastructure\Models\Product;
use Illuminate\Http\Request;
// In an Action or Service
public function findProductsWithoutBom(Request $request)
{
// Call the custom `hasNoBom()` method from ProductBuilder
$products = Product::query()
->hasNoBom()
->withCategory()
->get();
// Or apply standard filters from the request
$filteredProducts = Product::query()
->standardQuery($request)
->get();
return $products;
}This keeps query logic neatly encapsulated in ProductBuilder, so Action/Service code stays much cleaner and easier to follow.