Skip to content

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 where conditions, call a clearly named method such as Product::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.