mcpField
L
laravel-query-builders

laravel-query-builders

github.com/leeoveryDevelopment4340 installsUpdated 31w ago
Repository

About laravel-query-builders

A Laravel development skill that recommends custom Eloquent query builders instead of local scopes. It covers typed, chainable query methods, relationship queries, builder registration, and organizing reusable query logic.

Key features

  1. 01Custom Eloquent builder patterns
  2. 02Type-safe nested relationship queries
  3. 03Chainable query methods
  4. 04Reusable builder traits
  5. 05Model builder registration

Use cases

  1. 01Organizing order and customer filters
  2. 02Building typed relationship queries
  3. 03Reusing query logic across builders

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/leeovery/claude-laravel --skill laravel-query-builders

After installing, type/laravel-query-buildersin Claude Code to confirm it loaded.

Details

From the project README

Laravel Query Builders

Always use custom query builders instead of local scopes.

Related guides:

  • Models - Model integration with custom builders
  • Controllers - Using query objects in controllers

Why Custom Builders Over Scopes

❌ Do NOT use local scopes.

✅ Use custom query builders because they provide:

  1. Better type hinting - Full IDE autocomplete
  2. Type-safe nested queries - Type-hint closures in whereHas(), orWhereHas(), etc.
  3. Better organization - All query logic in one class
  4. More composable - Easier to chain and compose
  5. Easier testing - Test query logic in isolation

Basic Builder Structure

<?php

declare(strict_types=1);

namespace App\Builders;

use App\Enums\OrderStatus;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;

class OrderBuilder extends Builder
{
    public function wherePending(): self
    {
        return $this->where('status', OrderStatus::Pending);
    }

    public function whereCompleted(): self
    {
        return $this->where('status', OrderStatus::Completed);
    }

    public function whereCustomer(User|int $customer): self
    {
        $customerId = $customer instanceof User ? $customer->id : $customer;
        return $this->where('customer_id', $customerId);
    }

    public function whereTotalGreaterThan(int $amount): self
    {
        return $this->where('total', '>', $amount);
    }

    public function wherePlacedBetween(Carbon $start, Carbon $end): self
    {
        return $this->whereBetween('placed_at', [$start, $end]);
    }

    public function withRelated(): self
    {
        return $this->with(['customer', 'items.product', 'shipments']);
    }

    public function recent(): self
    {
        return $this->latest('placed_at');
    }
}

Type-Safe Nested Queries

Type-hint closures for full IDE support in relationship queries:

public function whereHasItems(array|string $productIds): self
{
    return $this->whereHas('items', function (OrderItemBuilder $query) use ($productIds): void {
        $query->whereIn('product_id', (array) $productIds);
    });
}

Usage:

Order::query()
    ->whereHas('items', function (OrderItemBuilder $query): void {
        $query->whereActive()        // Custom method - autocomplete works!
              ->whereProduct($id);   // Full type safety!
    })
    ->whereHas('customer', function (CustomerBuilder $query): void {
        $query->whereVerified()      // Custom method
              ->wherePremium();      // IDE knows all methods!
    })
    ->get();

PHPDoc for External Methods

Document methods from Spatie packages or macros:

/**
 * @method static OrderBuilder whereState(string $column, string|array $state)
 * @method static OrderBuilder whereNotState(string $column, string|array $state)
 */
class OrderBuilder extends Builder
{
    // ...
}

Builder Traits

Extract reusable query logic:

<?php

declare(strict_types=1);

namespace App\Builders\Concerns;

use Illuminate\Support\Arr;

trait HasProducts
{
    public function whereHasProducts(array|string $productIds): self
    {
        return $this->whereHas('products', function ($query) use ($productIds): void {
            $query->whereIn('id', Arr::wrap($productIds));
        });
    }

    public function whereHasActiveProducts(): self
    {
        return $this->whereHas('products', function ($query): void {
            $query->where('active', true);
        });
    }
}

Usage in builder:

class OrderBuilder extends Builder
{
    use HasProducts;

    // ...
}

Register Builder in Model

Preferred: PHP Attribute (Laravel 11+)
use Illuminate\Database\Eloquent\Attributes\UseEloquentBuilder;

#[UseEloquentBuilder(OrderBuilder::class)]
class Order extends Model
{
    // ...
}
Alternative: Static Property
class Order extends Model
{
    protected static string $eloquentBuilder = OrderBuilder::class;

    // ...
}
Deprecated: Method Override
// ❌ Don't use this approach anymore
public function newEloquentBuilder($query): OrderBuilder
{
    return new OrderBuilder($query);
}

Usage Examples

Basic Chaining
Order::query()
    ->wherePending()
    ->whereTotalGreaterThan(10000)
    ->wherePlacedBetween(now()->subWeek(), now())
    ->withRelated()
    ->recent()
    ->paginate();
Lazy Iteration
Order::query()
    ->whereCompleted()
    ->lazyById()
    ->each(function (Order $order): void {
        // Process order
    });
Complex Queries
$orders = Order::query()
    ->wherePending()
    ->whereCustomer($user)
    ->whereHasItems([$productId1, $productId2])
    ->wherePlacedBetween($startDate, $endDate)
    ->withRelated()
    ->get();

Empty Builders

Always create builders even if empty initially - for future extensibility:

<?php

declare(strict_types=1);

namespace App\Builders;

use Illuminate\Database\Eloquent\Builder;

class CustomerBuilder extends Builder
{
    // Empty for now, but ready for future methods
}

Common Builder Methods

Status Filtering
public function whereActive(): self
{
    return $this->where('status', 'active');
}

public function whereInactive(): self
{
    return $this->where('status', 'inactive');
}
Date Ranges
public function whereCreatedAfter(Carbon $date): self
{
    return $this->where('created_at', '>', $date);
}

public function whereCreatedToday(): self
{
    return $this->whereDate('created_at', today());
}
User/Owner Filtering
public function whereUser(User|int $user): self
{
    $userId = $user instanceof User ? $user->id : $user;
    return $this->where('user_id', $userId);
}
Relationship Loading
public function withFullRelations(): self
{
    return $this->with([
        'user',
        'items.product',
        'customer.address',
    ]);
}

Builder Organization

app/Builders/
├── OrderBuilder.php
├── CustomerBuilder.php
├── ProductBuilder.php
└── Concerns/
    ├── HasProducts.php
    ├── HasDates.php
    └── HasStatus.php

Summary

Custom query builders:

  • Provide better IDE support than scopes
  • Enable type-safe nested queries
  • Keep query logic organized
  • Are easier to test
  • Support method chaining

Never use local scopes - always use custom builders.

Comments

Markdown supported