From laravel-api-tool-kit
Adds filtering, sorting, search to existing model list endpoint: detects structure, creates/binds filter class, populates filters, updates controller.
npx claudepluginhub ahmedesa/laravel-api-tool-kitlaravel-api-tool-kit/workflows/# Workflow: Add Filtering to an Existing Model
Use this when an existing model needs filtering, sorting, or search added to its list endpoint.
---
## Step 0 — Detect Project Structure
- `Domain/` present → **DDD**: filters live in `app/Domain/{Model}/Filters/{Model}Filters.php`
- No `Domain/` → **Standard**: filters live in `app/Filters/{Model}Filters.php`
Check the existing model file to confirm — it will have `protected string $default_filters = ...` pointing to the correct namespace if a filter already exists.
---
## Step 1 — Check if a Filter class already exists
Standard: loo.../tableGenerates FilamentPHP v4 table configurations with columns, filters, search, sorting, row/bulk actions from a description. Supports --resource ResourceName [--for widget|relation-manager].
/laravel-api-resourcesImplements Laravel API Resources with pagination by exactly following the laravel:api-resources-and-pagination skill.
/laravelBuilds, configures, and optimizes Laravel apps with Eloquent models, service layers, queues, events, broadcasting, Sanctum/Passport auth, and Pest tests. Supports API, auth, queue, model gen, optimization, upgrade, audit flags.
Share bugs, ideas, or general feedback.
Use this when an existing model needs filtering, sorting, or search added to its list endpoint.
ls app/
Domain/ present → DDD: filters live in app/Domain/{Model}/Filters/{Model}Filters.phpDomain/ → Standard: filters live in app/Filters/{Model}Filters.phpCheck the existing model file to confirm — it will have protected string $default_filters = ... pointing to the correct namespace if a filter already exists.
Standard: look for app/Filters/{Model}Filters.php
DDD: look for app/Domain/{Model}/Filters/{Model}Filters.php
Follow rules/filters.md for the correct structure.
Standard: app/Filters/CarFilters.php
DDD: app/Domain/Car/Filters/CarFilters.php
Open the Model and ensure:
use App\Filters\CarFilters;
use Essa\APIToolKit\Filters\Filterable;
class Car extends Model
{
use Filterable;
protected string $default_filters = CarFilters::class;
}
Both use Filterable and protected string $default_filters are required.
Based on the requirement, fill in the appropriate arrays:
Add equality filters (?color=red&is_active=1):
protected array $allowedFilters = ['color', 'is_active', 'user_id'];
Add sort columns (?sorts=name or ?sorts=-created_at):
protected array $allowedSorts = ['name', 'created_at'];
Add eager-load includes (?includes=user,tags):
protected array $allowedIncludes = ['user', 'tags'];
Add text search columns (?search=keyword):
protected array $columnSearch = ['name', 'description'];
Add relationship search (?search=keyword):
protected array $relationSearch = [
'user' => ['first_name', 'last_name'],
];
Add a custom filter for non-trivial conditions:
// ?year=2023
public function year(string $term): void
{
$this->builder->whereYear('manufactured_at', $term);
}
Open the controller's index method and replace any manual query building with:
public function index(): AnonymousResourceCollection
{
return CarResource::collection(Car::useFilters()->dynamicPaginate());
}
Do NOT hardcode ->with(['user', 'tags']) here if those relationships are in $allowedIncludes. The client requests them via ?includes=user,tags and the filter class handles eager loading automatically. Hardcoding with() always loads relations even when not needed.
Filterable trait is on the model$default_filters is bound on the model$allowedFilters / $allowedSorts are populateduseFilters()->dynamicPaginate() — not ->get() or ->paginate()$this->builder — they return nothing