kdabrow / filters
Filters for the Laravel API
Installs: 12
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
Type:package
Requires
- php: >=8.2.0
- illuminate/contracts: ^10.0|^11.0|^12.0
- illuminate/database: ^10.0|^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0
This package is auto-updated.
Last update: 2025-08-22 12:52:11 UTC
README
A powerful filtering package for Laravel Eloquent models with support for dynamic queries, custom filters, nested conditions, ordering, and column aliases.
Features
- Laravel Model Integration: Direct filtering with
$model->filter()
- URL-based Filtering:
GET /users?f[0][c]=name&f[0][o]=like&f[0][v]=John
- Ordering Support:
GET /users?o[0][c]=created_at&o[0][v]=desc
- Column Aliases: Hide real database columns from your API
- Advanced Operators:
=
,!=
,>
,<
,>=
,<=
,like
,nlike
,in
,nin
,null
,nnull
- Nested Conditions: Complex grouping with AND/OR logic
- Security: Built-in input sanitization and SQL injection prevention
- Model Interfaces:
Filterable
andOrderable
for fine-grained control
Installation
composer require kdabrow/filters
The package includes Laravel integration out of the box. The service provider is auto-discovered.
Publish Configuration (Optional)
php artisan vendor:publish --tag=filters-config
Quick Start
Add the trait to your model and start filtering:
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Kdabrow\Filters\HasFilters; class User extends Model { use HasFilters; protected $fillable = ['name', 'email', 'status']; }
// In your controller class UserController extends Controller { public function index() { // Automatically filters from request parameters return User::filter() ->with('profile') ->paginate(15); } }
URL Examples:
GET /users?f[0][c]=name&f[0][o]=like&f[0][v]=John GET /users?f[0][c]=status&f[0][o]==&f[0][v]=active&o[0][c]=created_at&o[0][v]=desc
Model Configuration
Basic Model Setup
The simplest setup uses the $fillable
array for allowed columns:
class User extends Model { use HasFilters; protected $fillable = ['name', 'email', 'status', 'created_at']; // All fillable columns are available for filtering and ordering }
Advanced Model Configuration
For fine-grained control, implement the Filterable
and Orderable
interfaces:
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Kdabrow\Filters\HasFilters; use Kdabrow\Filters\Filterable; use Kdabrow\Filters\Orderable; class User extends Model implements Filterable, Orderable { use HasFilters; protected $fillable = ['name', 'email', 'status']; public function filters(): array { return [ 'name' => ['allowedOperators' => ['=', 'like']], 'email' => ['allowedOperators' => ['=', 'like']], 'status' => ['allowedOperators' => ['=', 'in']], 'created_at' => ['allowedOperators' => ['>=', '<=']], ]; } public function orders(): array { return [ 'name' => ['allowedDirections' => ['asc', 'desc']], 'email' => ['allowedDirections' => ['asc', 'desc']], 'created_at' => ['allowedDirections' => ['desc']], // Only descending ]; } }
Column Aliases
Hide real database column names from your API using aliases:
class Product extends Model implements Filterable, Orderable { use HasFilters; protected $fillable = ['product_name', 'price', 'category_id', 'is_active']; public function filters(): array { return [ // API alias => Real database column 'name' => ['allowedOperators' => ['=', 'like'], 'column' => 'product_name'], 'price' => ['allowedOperators' => ['=', '>=', '<=']], 'category' => ['allowedOperators' => ['=', 'in'], 'column' => 'category_id'], 'active' => ['allowedOperators' => ['='], 'column' => 'is_active'], ]; } public function orders(): array { return [ 'name' => ['allowedDirections' => ['asc', 'desc'], 'column' => 'product_name'], 'price' => ['allowedDirections' => ['asc', 'desc']], 'newest' => ['allowedDirections' => ['desc'], 'column' => 'created_at'], ]; } }
API Usage with Aliases:
# Filter by 'name' (maps to product_name column) GET /products?f[0][c]=name&f[0][o]=like&f[0][v]=iPhone # Order by 'newest' (maps to created_at column) GET /products?o[0][c]=newest&o[0][v]=desc
Controller Usage
Basic Usage
class UserController extends Controller { public function index() { // Automatically uses request parameters 'f' (filters) and 'o' (orders) return User::filter()->paginate(15); } }
Manual Parameters
public function customFilter() { $users = User::filter( input: [['c' => 'status', 'o' => '=', 'v' => 'active']], order: [['c' => 'created_at', 'v' => 'desc']] )->get(); return $users; }
Using Custom Filter Classes
public function advancedFilter() { return User::filter(name: UserFilter::class)->get(); }
Supported Operators
Operator | Description | Example |
---|---|---|
= |
Equal to | ['c' => 'status', 'o' => '=', 'v' => 'active'] |
!= |
Not equal to | ['c' => 'status', 'o' => '!=', 'v' => 'inactive'] |
> |
Greater than | ['c' => 'age', 'o' => '>', 'v' => '18'] |
< |
Less than | ['c' => 'age', 'o' => '<', 'v' => '65'] |
>= |
Greater than or equal | ['c' => 'price', 'o' => '>=', 'v' => '100'] |
<= |
Less than or equal | ['c' => 'price', 'o' => '<=', 'v' => '500'] |
like |
Contains (LIKE %value%) | ['c' => 'name', 'o' => 'like', 'v' => 'john'] |
nlike |
Does not contain | ['c' => 'email', 'o' => 'nlike', 'v' => 'temp'] |
in |
In array | ['c' => 'status', 'o' => 'in', 'v' => 'active,pending'] |
nin |
Not in array | ['c' => 'role', 'o' => 'nin', 'v' => 'banned,suspended'] |
null |
Is NULL | ['c' => 'deleted_at', 'o' => 'null'] |
nnull |
Is NOT NULL | ['c' => 'email_verified_at', 'o' => 'nnull'] |
URL Structure
Filter Parameters (f)
# Basic filter GET /users?f[0][c]=name&f[0][o]==&f[0][v]=John # Multiple filters (AND by default) GET /users?f[0][c]=name&f[0][o]=like&f[0][v]=John&f[1][c]=status&f[1][o]==&f[1][v]=active # OR condition GET /users?f[0][c]=role&f[0][o]==&f[0][v]=admin&f[1][c]=role&f[1][o]==&f[1][v]=moderator&f[1][t]=or
Order Parameters (o)
# Single column ascending (default) GET /users?o[0][c]=name # Single column descending GET /users?o[0][c]=created_at&o[0][v]=desc # Multiple columns GET /users?o[0][c]=name&o[0][v]=asc&o[1][c]=created_at&o[1][v]=desc
Advanced Features
Nested Conditions
Create complex grouped conditions:
$filters = [ [ // Group 1: (status=0 AND name='test' OR name='test 2') ['c' => 'status', 'o' => '=', 'v' => '0'], ['c' => 'name', 'o' => '=', 'v' => 'test'], ['c' => 'name', 'o' => '=', 'v' => 'test 2', 't' => 'or'], ], [ // Group 2: (status=1 AND name='test 3') ['c' => 'status', 'o' => '=', 'v' => '1'], ['c' => 'name', 'o' => '=', 'v' => 'test 3'], 't' => 'or' // Connect this group with OR ] ]; User::filter(input: $filters)->get();
Generated SQL:
WHERE (status = '0' AND name = 'test' OR name = 'test 2') OR (status = '1' AND name = 'test 3')
Custom Filter Classes
Create custom filter classes for complex filtering logic that goes beyond simple column comparisons:
<?php namespace App\Filters; use Kdabrow\Filters\Filter; class UserFilter extends Filter { protected function filterActive($builder) { if (! reqeust()->has('is_verified')) { return; } $builder->where('status', 'active') ->whereNotNull('verified_at'); } protected function filterAdmins($builder) { if (! reqeust()->has('is_admin')) { return; } $builder->whereIn('role', ['admin', 'super_admin']); } protected function filterPremium($builder) { if (! reqeust()->has('is_premium')) { return; } $builder->whereHas('subscription', function($query) { $query->where('type', 'premium') ->where('expires_at', '>', now()); }); } }
Auto-Discovery Usage:
The package automatically discovers filter classes based on your model name:
// For User model, it looks for App\Filters\UserFilter User::filter()->get();
Manual Class Specification:
// Explicitly specify the filter class User::filter(name: UserFilter::class)->get(); // Use a different filter class than the auto-discovered one User::filter(name: AdminUserFilter::class)->get(); // Pass an instance instead of class name $filter = new UserFilter(); User::filter(name: $filter)->get();
How Auto-Discovery Works:
- Takes your model class name (e.g.,
User
,BlogPost
) - Prepends the configured namespace (default:
App\Filters
) - Appends
Filter
suffix - Checks if the class exists and extends the base
Filter
class - If found, instantiates and uses it; otherwise falls back to dynamic filtering
Filter Methods:
- Method names must start with
filter
(e.g.,filterActive
,filterPremium
) - Each method receives the query
$builder
as parameter - Methods are automatically called when the filter is applied
Configuration
The config/filters.php
file allows customization:
return [ // Request parameter key for filters (default: 'f') 'key' => 'f', // Request parameter key for ordering (default: 'o') 'order_key' => 'o', // Default namespace for filter classes 'namespace' => 'App\\Filters', // Enable/disable auto-discovery of filter classes 'auto_discovery' => true, ];
Security Features
- Column Validation: Only defined columns (fillable or interface) are allowed
- Operator Validation: Only specified operators per column are accepted
- Direction Validation: Only allowed directions per column are accepted
- Input Sanitization: All values are sanitized to prevent SQL injection
- Column Sanitization: Column names are sanitized to prevent injection
Manual Usage (Without Models)
For non-Laravel projects or custom usage:
use Kdabrow\Filters\Filter; use Kdabrow\Filters\Ordering; $filters = [ ['c' => 'name', 'o' => 'like', 'v' => 'John'], ['c' => 'status', 'o' => '=', 'v' => 'active'], ]; $orders = [ ['c' => 'created_at', 'v' => 'desc'] ]; // Apply filters $filter = new Filter(); $filter->load($filters, $model); $filter->apply($queryBuilder); // Apply ordering $ordering = new Ordering(); $ordering->load($orders, $model); $ordering->apply($queryBuilder);
Complete Example
<?php namespace App\Http\Controllers; use App\Models\User; use Illuminate\Http\Request; class UserController extends Controller { public function index() { return User::filter() ->with(['posts', 'profile']) ->whereHas('posts', function ($query) { $query->where('published', true); }) ->paginate(request('per_page', 15)); } }
Request Examples:
# Search active users named John, ordered by creation date GET /users?f[0][c]=name&f[0][o]=like&f[0][v]=John&f[1][c]=status&f[1][o]==&f[1][v]=active&o[0][c]=created_at&o[0][v]=desc # Complex nested condition with ordering GET /users?f[0][0][c]=role&f[0][0][o]==&f[0][0][v]=admin&f[0][1][c]=status&f[0][1][o]==&f[0][1][v]=active&f[1][0][c]=role&f[1][0][o]==&f[1][0][v]=user&f[1][t]=or&o[0][c]=name&o[0][v]=asc