indexzer0/eloquent-filtering
Filter Laravel Eloquent models using simple arrays and request data—no custom query spaghetti. Define allowed filters on your models, support complex search, and keep queries readable, maintainable, and easy to extend for APIs and dashboards.
Install the package:
composer require indexzer0/eloquent-filtering
php artisan eloquent-filtering:install
Implement IsFilterable in your model:
use IndexZer0\EloquentFiltering\Contracts\IsFilterable;
use IndexZer0\EloquentFiltering\Traits\Filterable;
class Product extends Model implements IsFilterable
{
use Filterable;
public function allowedFilters(): AllowedFilterList
{
return Filter::only(
Filter::field('name', [FilterType::EQUAL]),
Filter::field('price', [FilterType::GREATER_THAN, FilterType::LESS_THAN])
);
}
}
First use case:
$products = Product::filter([
['type' => '$eq', 'target' => 'name', 'value' => 'TV'],
['type' => '$gt', 'target' => 'price', 'value' => 100]
])->get();
allowedFilters(): Define which filters are permitted on the model.Filter::field(): Specify allowed filter types for a field (e.g., EQUAL, LIKE, GREATER_THAN).Filter::only(): Restrict filters to a predefined list.Filter::all(): Allow all filter types for a field.Filter::custom(): Define custom filter logic.Workflow:
request()->all() or request()->query()).$filters = request()->input('filters', []);
$results = Product::filter($filters)->get();
Tip: Use Filter::custom() for complex logic:
Filter::custom('active', function (Builder $query, $value) {
return $query->where('is_active', $value);
});
Pattern: Combine with Laravel Scout or custom search:
$users = User::filter([
['type' => '$like', 'target' => 'name', 'value' => 'John'],
['type' => '$gt', 'target' => 'created_at', 'value' => now()->subDays(30)->toDateString()]
])->paginate(10);
Tip: Use Filter::field() with modifiers for partial matches:
Filter::field('email', [FilterType::LIKE]) // Supports `$like:start` and `$like:end`
Pattern: Filter related models via JSON paths:
Filter::field('user->role->name', [FilterType::EQUAL]) // Requires JSON column or relationship
Tip: For JSON columns, use FilterType::JSON_LENGTH:
Filter::field('metadata->tags', [FilterType::JSON_LENGTH])
$and, $or)Pattern: Group filters logically:
$filters = [
['type' => '$or', 'filters' => [
['type' => '$eq', 'target' => 'status', 'value' => 'active'],
['type' => '$eq', 'target' => 'status', 'value' => 'pending']
]],
['type' => '$and', 'filters' => [
['type' => '$gt', 'target' => 'price', 'value' => 50],
['type' => '$lt', 'target' => 'price', 'value' => 200]
]]
];
Note: $and/$or are always allowed and don’t need explicit definition.
Pattern: Validate filters before applying:
use Illuminate\Support\Facades\Validator;
$validator = Validator::make($request->all(), [
'filters' => 'array',
'filters.*' => 'required|array',
'filters.*.type' => 'required|string',
'filters.*.target' => 'required|string',
'filters.*.value' => 'sometimes|string|numeric',
]);
Pattern: Cache filtered queries with tags:
$cacheKey = 'products_' . md5(serialize($filters));
return Cache::remember($cacheKey, now()->addHours(1), function () use ($filters) {
return Product::filter($filters)->get();
});
SQL Injection Risk:
target fields (e.g., ensure they match allowed columns).Filter::field() to restrict targets to model attributes.Performance with Complex Filters:
$or/$and) in large datasets. Test with toSql() to review generated queries.price, created_at).JSON Path Limitations:
user->role->name) require MySQL 5.7+ or PostgreSQL.toSql() to debug path syntax.Case Sensitivity:
LIKE filters are case-sensitive by default. Use LOWER() in custom filters for case-insensitive searches:
Filter::custom('name', function (Builder $query, $value) {
return $query->whereRaw('LOWER(name) LIKE LOWER(?)', ["%{$value}%"]);
});
Sorting (Experimental):
$results = Product::filter($filters)->orderBy('name')->get();
Inspect Generated SQL:
$query = Product::filter($filters);
dd($query->toSql(), $query->getBindings());
Check Allowed Filters:
dd(Product::allowedFilters()); // Debug allowed filter definitions
Handle Missing Fields:
Filter::field() with fallback logic:
Filter::field('optional_field', [FilterType::EQUAL], function () {
return FilterType::EQUAL; // Default if field doesn’t exist
});
Custom Filter Types:
Filter::extend('custom_type', function (Builder $query, $target, $value) {
return $query->where($target, 'custom_logic', $value);
});
Override Default Behavior:
php artisan vendor:publish --tag=eloquent-filtering-config) and modify:
'default_filter_types' => [
FilterType::EQUAL,
FilterType::LIKE,
// Add/remove types
],
Integrate with Scout:
Filter::custom() to bridge with Scout’s search logic:
Filter::custom('scout_search', function (Builder $query, $value) {
return $query->where('name', 'LIKE', "%{$value}%");
});
Case-Insensitive Targets:
config/eloquent-filtering.php:
'case_insensitive_targets' => ['name', 'email'], // Auto-convert to LOWER()
Disable Default Filters:
'default_filter_types' => [], // Disable all default types
JSON Path Wildcards:
'json_path_wildcards' => true,
How can I help you explore Laravel packages today?