lacodix/laravel-model-filter
Filter, search, and sort Eloquent models with reusable filter classes and query-string support. Includes built-in types (string, date, number, enum), relation/nested relation filtering, custom complex logic, and filter visualisation.
Installation:
composer require lacodix/laravel-model-filter
Publish config (if needed):
php artisan vendor:publish --provider="Lacodix\LaravelModelFilter\ServiceProvider"
First Filter:
Generate a filter for a date field (e.g., created_at):
php artisan make:filter CreatedAfterFilter --type=date --field=created_at
Apply to Model:
Add the HasFilters trait and register the filter in your model (e.g., Post):
use Lacodix\LaravelModelFilter\Traits\HasFilters;
class Post extends Model
{
use HasFilters;
protected array $filters = [
\App\Models\Filters\CreatedAfterFilter::class,
];
}
First Query: Filter posts created after January 1, 2023:
Post::filter(['created_after_filter' => '2023-01-01'])->get();
Or via URL:
/posts?created_after_filter=2023-01-01
Search Setup:
Enable search for a model (e.g., Post):
use Lacodix\LaravelModelFilter\Traits\IsSearchable;
class Post extends Model
{
use IsSearchable;
protected array $searchable = ['title', 'content'];
}
Search for "test":
Post::search('test')->get();
Or via URL:
/posts?search=test
Filter Creation:
make:filter for common types (date, text, select, etc.).DateFilter, TextFilter) for custom logic.StatusFilter for enum-like fields:
class StatusFilter extends SelectFilter
{
protected string $field = 'status';
public function options(): array
{
return ['draft', 'published', 'archived'];
}
}
Query Integration:
Post::filter(['status_filter' => 'published'])->get();
Post::filterByQueryString()->get(); // Parses request query
Post::filter(['hot_filter' => 'true'], 'frontend')->get();
Search Patterns:
Post::search('laravel')->get();
Post::search('laravel', ['title'])->get();
protected array $searchable = [
'title' => SearchMode::STARTS_WITH_CASE_SENSITIVE,
];
Relation Filtering:
RunsOnRelation trait for nested filters:
class Comment extends Model
{
use HasFilters, RunsOnRelation;
protected array $filters = [
\App\Models\Filters\AuthorFilter::class,
];
}
Post::whereHas('comments', fn($q) =>
$q->filter(['author_filter' => 'john'])
)->get();
Visualization:
<x-lacodix-filter::model-filters :model="Post::class" />
<x-lacodix-filter::model-filters
:model="Post::class"
method="post"
:action="route('posts.filter')"
/>
Dynamic Filtering:
public function visible(): bool
{
return Feature::active('advanced_filters');
}
API Usage:
$filters = request()->query('filter');
$posts = Post::filter($filters)->get();
filterByQueryString() for automatic parsing.Form Integration:
<form wire:submit.prevent="filter">
<x-lacodix-filter::model-filters :model="$posts" />
<button type="submit">Apply</button>
</form>
Testing:
$filter = new CreatedAfterFilter();
$this->assertEquals('created_at', $filter->field());
$query = Post::filterByQueryString();
$query->toSql(); // Verify generated SQL
Performance:
select() to limit fetched columns:
Post::filter($filters)->select(['id', 'title'])->get();
N+1 issues with eager loading:
Post::with('comments')->filter($filters)->get();
Custom Components:
protected string $component = 'custom-filter';
php artisan vendor:publish --tag=lacodix-filter-views
Field Name Mismatches:
$field in filters matches the database column name.user.name for user relation).Query String Parsing:
filter[] for filters and search for searches.config/model-filter.php:
'filter_query_value_name' => 'f',
'search_query_value_name' => 'q',
Case Sensitivity:
LIKE_CASE_SENSITIVE may impact performance.Grouping Confusion:
// ❌ Fails silently
Post::filter(['hot_filter' => 'true'])->get();
// ✅ Works
Post::filter(['hot_filter' => 'true'], 'frontend')->get();
Relation Filtering:
RunsOnRelation requires proper field qualification. For example:
// ❌ Fails (ambiguous column)
$q->filter(['author_filter' => 'john']);
// ✅ Works (qualified)
$q->filter(['comments.author_filter' => 'john']);
Search Performance:
CONTAINS_ALL/CONTAINS_ANY on large text fields without full-text indexes.tsvector).Filter Mode Limitations:
BETWEEN for TextFilter).Log Generated SQL:
\DB::enableQueryLog();
Post::filter($filters)->get();
\Log::info(DB::getQueryLog());
Inspect Filter Inputs:
dd(request()->query());
Validate Filter Values:
validate() in custom filters:
public function validate(string $value): void
{
if (!in_array($value, $this->options())) {
throw new \InvalidArgumentException("Invalid status: {$value}");
}
}
Check Visibility:
visible() method:
$filter = new CreatedAfterFilter();
dd($filter->visible()); // Should return bool
Custom Filter Types:
BaseFilter for new types:
class CustomFilter extends BaseFilter
{
protected string $type = 'custom';
public function apply(Builder $query, string $value): void
{
// Custom logic
}
}
Dynamic Field Mapping:
field() to map input to database columns:
public function field(): string
{
return $this->input === 'user_name' ? 'users.name' : $this->field;
}
How can I help you explore Laravel packages today?