A Laravel package for filterable traits and classes. This package provides powerful, dynamic query filtering capabilities directly from incoming requests, especially useful when developing flexible and dynamic APIs.
- Easy Integration: Apply the
Filterabletrait to your Eloquent models. - Comprehensive Filters: Support for 15+ filter types including exact, like, ilike, in, between, greater/less than, negation filters (notEquals, notIn, notLike), null checks (isNull, isNotNull), and text pattern filters (startsWith, endsWith).
- Database Compatibility: Database-specific optimizations for PostgreSQL, MySQL, and SQLite.
- Dynamic Sorting: Customize sorting behavior directly from requests.
- Relationship Filters: Use advanced conditional logic like
whereAny,whereAll, andwhereNonefor relational queries. - JSON Support: Directly filter JSON columns with dot-notation.
- Performance Optimizations: Built-in caching and efficient query construction.
- Date Handling: Smart handling of date fields with Carbon integration.
composer require devaction-labs/filterable-packagenamespace App\Models;
use Illuminate\Database\Eloquent\Model;
use DevactionLabs\FilterablePackage\Traits\Filterable;
class Expense extends Model
{
use Filterable;
protected array $filterMap = [
'search' => 'description',
'date' => 'expense_date',
];
protected array $allowedSorts = ['expense_date', 'amount'];
}namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use DevactionLabs\FilterablePackage\Filter;
use App\Models\Expense;
class ExpenseController extends Controller
{
public function index()
{
$expenses = Expense::query()
->filtrable([
Filter::like('description', 'search'),
Filter::exact('expense_date', 'date'),
Filter::between('expense_date', 'date_range'),
Filter::json('attributes', 'user.name', 'LIKE', 'user_name'),
Filter::json('attributes', 'user.age', '>', 'user_age'),
Filter::relationship('user', 'name')->setValue('John')->with(),
Filter::relationship('user', 'name')->whereAny([
['name', '=', 'John'],
['email', '=', '[email protected]'],
])->with(),
])
->customPaginate('paginate', 10, ['per_page' => 10, 'sort' => '-created_at']);
return response()->json($expenses);
}
}- Exact Match:
Filter::exact('status', 'status') - Not Equals:
Filter::notEquals('status', 'exclude_status') - Greater Than:
Filter::gt('amount', 'min_amount') - Greater Than or Equal:
Filter::gte('amount', 'min_amount') - Less Than:
Filter::lt('amount', 'max_amount') - Less Than or Equal:
Filter::lte('amount', 'max_amount') - Between:
Filter::between('created_at', 'date_range')
- LIKE Match:
Filter::like('description', 'search') - Case-Insensitive LIKE:
Filter::ilike('description', 'search')(Database-specific) - NOT LIKE:
Filter::notLike('description', 'exclude_text') - Starts With:
Filter::startsWith('name', 'name_prefix') - Ends With:
Filter::endsWith('email', 'email_suffix')
- IN Clause:
Filter::in('category_id', 'categories') - NOT IN Clause:
Filter::notIn('status', 'exclude_statuses')
- Is Null:
Filter::isNull('deleted_at', 'show_deleted') - Is Not Null:
Filter::isNotNull('email_verified_at', 'verified_only')
The ilike() filter automatically adapts to your database:
- PostgreSQL: Uses native
ILIKEoperator - SQLite: Falls back to
LIKE(case-sensitive) - MySQL: Uses
LOWER()function for case-insensitive comparison
// Example usage for case-insensitive search
$filters = [
Filter::ilike('name', 'search'), // Works across all databases
];- Exact Match:
Filter::json('data', 'user.name', '=', 'user_name') - LIKE Match:
Filter::json('data', 'user.name', 'LIKE', 'user_name')
-
Simple Relationship:
Filter::relationship('user', 'name')->setValue('John')->with()
-
Conditional Logic (
whereAny,whereAll,whereNone):Filter::relationship('user', 'name') ->whereAny([ ['name', '=', 'John'], ['email', '=', '[email protected]'], ]) ->setValue('John') ->with();
The package provides flexible pagination options through the customPaginate method, supporting three pagination types:
$results = Expense::query()
->filtrable([...])
->customPaginate('paginate', 15);
// Returns: total, last_page, current_page, per_page, etc.$results = Expense::query()
->filtrable([...])
->customPaginate('simple', 15);
// Returns: current_page, per_page, next_page_url, prev_page_url (no total)$results = Expense::query()
->filtrable([...])
->customPaginate('cursor', 15);
// Returns: cursor-based navigation (ideal for infinite scroll)You can pass custom data to append to pagination links:
$results = Expense::query()
->filtrable([...])
->customPaginate('paginate', 15, [
'per_page' => 15,
'sort' => '-created_at'
]);Sorting:
-(minus) prefix indicates descending sorting (e.g.,-amount)- Ascending sort uses the field name directly (e.g.,
amount)
protected string $defaultSort = 'amount';
protected array $allowedSorts = ['amount', 'expense_date'];Easily map request parameters to database columns:
protected array $filterMap = [
'display_name' => 'name',
'date' => 'expense_date',
];Now, using the parameter filter[display_name]=John will filter on the name column.
The Filterable package provides sophisticated date handling capabilities:
// Create a date filter that will convert string dates to Carbon instances
$dateFilter = Filter::exact('created_at')->castDate();
// Apply to a query
$model->filtrable([$dateFilter]);You can also specify if you want to compare with the start or end of the day:
// Filter by date with time set to 23:59:59
$dateFilter = Filter::exact('created_at')->castDate()->endOfDay();
// Filter by date with time set to 00:00:00
$dateFilter = Filter::exact('created_at')->castDate()->startOfDay();Customize the pattern used for LIKE filters to match your search requirements:
// Default (contains): '%value%'
$filter = Filter::like('description', 'search');
// Starts with: 'value%'
$filter = Filter::like('description', 'search')->setLikePattern('{{value}}%');
// Ends with: '%value'
$filter = Filter::like('description', 'search')->setLikePattern('%{{value}}');The package automatically applies the correct JSON extraction syntax based on your database:
// The query will use the appropriate syntax for your database
$filter = Filter::json('attributes', 'user.age', '>', 'min_age');
// Manually specify database driver if needed
$filter = Filter::json('attributes', 'user.age', '>', 'min_age')->setDatabaseDriver('mysql');Apply complex conditions to your relationship filters:
// Match if ANY condition is true (OR logic)
$filter = Filter::relationship('user', 'name')
->whereAny([
['name', '=', 'John'],
['email', '=', '[email protected]'],
])
->with();
// Match if ALL conditions are true (AND logic)
$filter = Filter::relationship('user', 'name')
->whereAll([
['name', '=', 'John'],
['active', '=', true],
])
->with();
// Match if NONE of the conditions are true (NOT logic)
$filter = Filter::relationship('user', 'name')
->whereNone([
['banned', '=', true],
['deleted', '=', true],
])
->with();The Filterable trait includes several performance optimizations:
- Efficient caching of attribute and relationship validations
- Optimized handling of relationship filters
- Smart deduplication of eager-loaded relationships
- Specialized handling for simple equality relationship filters
These optimizations are automatically applied when you use the trait, ensuring your filterable queries remain performant even with complex filter combinations.
Here's a comprehensive example showing how to use multiple features together:
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Product;
use DevactionLabs\FilterablePackage\Filter;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function index(Request $request)
{
// Build a complex query using the Filterable trait
$products = Product::query()
// Define allowed sort fields and default sort
->allowedSorts(['name', 'price', 'created_at'], '-created_at')
// Define filter field mappings
->filterMap([
'search' => 'name',
'price_range' => 'price',
'date' => 'created_at',
'status_code' => 'status',
])
// Apply filters
->filtrable([
// Basic filters
Filter::like('name', 'search')
->setLikePattern('{{value}}%'), // Custom LIKE pattern (starts with)
// Numeric range filter
Filter::between('price', 'price_range'),
// Date filter with Carbon conversion
Filter::exact('created_at', 'date')
->castDate()
->endOfDay(), // Automatically set time to end of day
// JSON field filtering
Filter::json('attributes', 'specs.color', 'LIKE', 'color')
->setDatabaseDriver('mysql'),
Filter::json('attributes', 'specs.weight', '>', 'min_weight')
->setDatabaseDriver('mysql'),
// Relationship filter with eager loading
Filter::relationship('category', 'slug', '=', 'category')
->with(), // Eager load this relationship
// Complex relationship filter with conditional logic
Filter::relationship('tags', 'name')
->whereAny([
['name', '=', 'featured'],
['name', '=', 'sale'],
])
->with()
->setValue('has_special_tag'), // Custom value for this filter
// Multiple criteria for user permissions
Filter::relationship('user', 'id')
->whereAll([
['active', '=', true],
['role', '=', 'admin'],
])
->setValue(auth()->id()),
])
// Apply pagination with custom parameters
->customPaginate('paginate', $request->input('per_page', 15), [
'per_page' => $request->input('per_page', 15),
'sort' => $request->input('sort', '-created_at'),
]);
return response()->json($products);
}
}- MySQL
- PostgreSQL
- SQLite
The package automatically detects the database driver from your configuration.
composer testPlease see CONTRIBUTING.md for details.
If you discover any security related issues, please email [email protected] instead of using the issue tracker.
The MIT License (MIT). Please see License File for more information.