ptplugins / filament-auto-filters
Automatic table filters for FilamentPHP based on column definitions
Package info
github.com/ptplugins/filament-auto-filters
pkg:composer/ptplugins/filament-auto-filters
Requires
- php: ^8.2
- filament/filament: ^3.0 || ^4.0 || ^5.0
- illuminate/support: ^10.0 || ^11.0 || ^12.0
README
Automatic table filters for FilamentPHP v3, v4, and v5 based on your column definitions. Stop writing repetitive filter code โ just define your columns and get smart filters for free.
Single codebase across all three Filament major versions โ same trait, same API.
๐ฏ Try it live ยท ptplugins.com/demo/auto-filters โ no signup, click any column, see filters generate themselves.
The Problem
Every Filament resource needs filters. For a typical table with 10 columns, you end up writing 10 filter definitions โ most of which follow the same patterns. Date columns get date pickers, text columns get search inputs, relationships get whereHas queries.
Before (manual filters for each column):
public static function table(Table $table): Table { return $table ->columns([ TextColumn::make('name'), TextColumn::make('email'), TextColumn::make('department.name'), TextColumn::make('hired_at')->date(), TextColumn::make('data.position'), ]) ->filters([ Filter::make('name') ->form([TextInput::make('value')->label('Name')]) ->query(fn (Builder $q, array $data) => /* ... */), Filter::make('email') ->form([TextInput::make('value')->label('Email')]) ->query(fn (Builder $q, array $data) => /* ... */), Filter::make('department.name') ->form([TextInput::make('value')->label('Department')]) ->query(fn (Builder $q, array $data) => $q->whereRelation(/* ... */)), Filter::make('hired_at') ->form([DatePicker::make('from'), DatePicker::make('until')]) ->query(fn (Builder $q, array $data) => /* ... */), Filter::make('data.position') ->form([TextInput::make('value')->label('Position')]) ->query(fn (Builder $q, array $data) => /* ... using JSON arrow notation */), ]); }
After (one line):
->filters(static::autoFilters($table))
Installation
composer require ptplugins/filament-auto-filters
The package auto-discovers its service provider. No manual registration needed.
Publish Configuration (optional)
php artisan vendor:publish --tag=auto-filters-config
Quick Start
Add the trait to your Filament resource and call autoFilters():
use PtPlugins\FilamentAutoFilters\Concerns\HasAutoFilters; class EmployeeResource extends Resource { use HasAutoFilters; public static function table(Table $table): Table { return $table ->columns([/* ... */]) ->filters(static::autoFilters($table)); } }
That's it. Every TextColumn in your table now has a filter.
Full Example: Employee Management
Let's walk through a real-world scenario. You're building an HR module with an Employee model that has direct columns, a department relationship, and a JSON data column for flexible fields.
The Model
class Employee extends Model { // Direct columns const NAME = 'name'; const EMAIL = 'email'; const HIRED_AT = 'hired_at'; const SALARY = 'salary'; // JSON data column fields const D_POSITION = 'data.position'; const D_OFFICE = 'data.office'; // Relationships const R_DEPARTMENT_NAME = 'department.name'; const R_MANAGER_NAME = 'manager.name'; protected $casts = [ 'hired_at' => 'date', 'data' => 'array', ]; public function department(): BelongsTo { return $this->belongsTo(Department::class); } public function manager(): BelongsTo { return $this->belongsTo(Employee::class, 'manager_id'); } }
The Resource
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; use PtPlugins\FilamentAutoFilters\Concerns\HasAutoFilters; class EmployeeResource extends Resource { use HasAutoFilters; protected static ?string $model = Employee::class; public static function table(Table $table): Table { return $table ->columns([ TextColumn::make(Employee::NAME) ->label('Name') ->searchable(), TextColumn::make(Employee::EMAIL) ->label('Email'), TextColumn::make(Employee::R_DEPARTMENT_NAME) ->label('Department'), TextColumn::make(Employee::R_MANAGER_NAME) ->label('Manager'), TextColumn::make(Employee::HIRED_AT) ->label('Hired') ->date(), TextColumn::make(Employee::SALARY) ->label('Salary') ->numeric(), TextColumn::make(Employee::D_POSITION) ->label('Position'), TextColumn::make(Employee::D_OFFICE) ->label('Office'), ]) ->filters(static::autoFilters($table)); } }
What Gets Generated
The plugin inspects each column and generates the right filter type. Detection rules:
| Column | Filter |
|---|---|
TextColumn (date / datetime) |
Date range picker (from / until) |
TextColumn (default) |
Text search (LIKE %...%) |
TextColumn with dot notation rel.col |
Text search via whereRelation() |
TextColumn with data.X prefix |
Text search via JSON arrow data->X |
IconColumn->boolean() |
Ternary (Yes / No / All) |
SelectColumn->options([...]) |
Select filter with same options |
| Other column types | Skipped โ pass an explicit filter via overrides |
For our Employee example (8 columns), all 8 filters are generated from zero lines of filter code. Same applies to a table mixing IconColumn and SelectColumn โ they get their right filter automatically.
Overriding Specific Filters
Auto-generated filters are great for most columns. But sometimes you need a SelectFilter with specific options, or custom logic. Pass your explicit filters as overrides โ they replace any auto-generated filter with the same name:
->filters(static::autoFilters($table, overrides: [ // Replace the auto-generated text filter for department // with a select dropdown instead static::makeSelectFilter( Employee::R_DEPARTMENT_NAME, 'Department', Department::pluck('name', 'name') ), // Custom filter with your own logic SelectFilter::make(Employee::SALARY) ->label('Salary Range') ->options([ 'junior' => 'Under 50k', 'mid' => '50k - 100k', 'senior' => 'Over 100k', ]), ]))
Result: department.name and salary use your custom filters. The remaining 6 columns still get auto-generated filters.
Skipping Columns
Some columns don't need filters. Pass their names in the skip array:
->filters(static::autoFilters($table, skip: [ 'deleted_at', Employee::SALARY, ]))
Combining Overrides and Skips
->filters(static::autoFilters($table, overrides: [ static::makeSelectFilter( Employee::R_DEPARTMENT_NAME, 'Department', Department::pluck('name', 'name') ), ], skip: [ 'deleted_at', ] ))
Priority order:
- Override filters are always included first
- Columns matching an override name are skipped (no duplicates)
- Columns in the
skiplist are skipped - Everything else gets an auto-generated filter
API Reference
autoFilters(Table $table, array $overrides = [], array $skip = []): array
The main method. Inspects every column in the table and generates an appropriate filter for TextColumn, IconColumn->boolean(), and SelectColumn. Other column types are skipped โ pass them explicitly via overrides.
makeTernaryFilter(string $name, string $label): TernaryFilter
Creates a yes/no/all ternary filter for a boolean column. Handles direct, JSON, and relationship columns the same way as the other helpers.
makeSelectFilter(string $name, string $label, array|Closure $options): SelectFilter
Creates a select dropdown filter that automatically handles:
- Direct columns โ standard
whereInquery - JSON columns (
data.xxx) โ usesattribute()with arrow notation - Relationship columns (
rel.col) โ useswhereHasquery
By default, select filters are multiple-choice and searchable (configurable).
makeDateRangeFilter(string $name, string $label): Filter
Creates a date range filter with "from" and "until" date pickers. Handles relationship and JSON columns the same way.
makeTextFilter(string $name, string $label): Filter
Creates a text search filter (LIKE contains). Handles relationship and JSON columns automatically.
resolveColumn(string $name): array
Resolves a column name into its type and query components. Used internally, but available if you need to build custom filters with the same column-detection logic.
Returns:
['type' => FilterType::Direct, 'query_column' => 'name']['type' => FilterType::Relationship, 'relationship' => 'department', 'column' => 'name']['type' => FilterType::Json, 'query_column' => 'data->position']
Configuration
Publish the config file to customize defaults:
php artisan vendor:publish --tag=auto-filters-config
// config/auto-filters.php return [ 'date_filter_columns' => 3, // Form column layout for date range 'text_search_placeholder' => 'Search...', // Placeholder for text inputs 'date_format' => 'd.m.Y', // Display format in filter indicators 'select_multiple' => true, // Allow multi-select by default 'select_searchable' => true, // Searchable dropdowns by default ];
Recommended UX: Slide-Over Panel + Inline Labels
Out of the box Filament renders filters as a dropdown attached to a small "Filter" button โ fine for one or two filters, cramped once a table has six or more. With auto-generated filters every column suddenly has a filter, so the dropdown stops scaling.
The trait already renders form fields with inlineLabel() (label on the left of the input, single row per filter). Pair that with a slide-over panel on the Resource and you get a "Linear / Notion"-style filter sidebar โ same setup we ship with our live demo:
use Filament\Tables\Table; public static function table(Table $table): Table { return $table ->columns([/* ... */]) ->filters(static::autoFilters($table)) ->filtersFormWidth('md') // sm | md | lg | xl ->filtersTriggerAction( fn ($action) => $action->slideOver(), // open on the right ); }
What changes for the user:
- Click Filter โ panel slides in from the right.
- Each filter is one row:
Label [input](frominlineLabel()). - Apply / Reset live in the panel footer; close =
Escor click outside. - Table reclaims the vertical space the old dropdown took.
This is purely Filament's Table API โ no extra CSS, no Livewire plumbing. The trait stays focused on filter generation; this snippet is the matching presentation.
Uniform inline labels across every filter type
Every auto-generated filter renders with an inline label on the left and the input on the right, including the awkward cases that don't accept inlineLabel() directly:
| Filter type | How inline label is applied |
|---|---|
Text search (makeTextFilter) |
TextInput::inlineLabel() in the filter's ->form() schema |
Date range (makeDateRangeFilter) |
Two stacked rows (Date from, Date until) โ both DatePicker::inlineLabel(). Avoids the asymmetric "label only on first input" layout that a single-row layout produces. |
Select (makeSelectFilter) |
SelectFilter::modifyFormFieldUsing(fn ($f) => $f->inlineLabel()) (SelectFilter doesn't have a ->form() we can put inlineLabel() on directly) |
Ternary (makeTernaryFilter) |
Same as Select โ TernaryFilter::modifyFormFieldUsing(fn ($f) => $f->inlineLabel()). Filament's TernaryFilter builds its own form schema, so the regular inlineLabel() on a child component would never reach it. |
Result: every row in the slide-over reads as [Label] [input] โ same vertical rhythm whether the column behind it is text, date, boolean, select, JSON, or a relationship.
If you write a custom filter (
Filter::make()->form([...])outside this trait) and want it to match, just add->inlineLabel()to every form component in the schema.
How Column Detection Works
The plugin uses a simple naming convention to detect column types:
name โ Direct column โ WHERE name LIKE '%...%'
hired_at โ Date column โ WHERE hired_at >= ? AND hired_at <= ?
department.name โ Relationship โ whereHas('department', fn($q) => $q->where('name', ...))
data.position โ JSON column โ WHERE data->position LIKE '%...%'
- Dot notation with a
data.prefix โ JSON arrow notation - Dot notation without
data.prefix โ Eloquent relationship - No dots โ direct database column
- Date/DateTime detection uses Filament's built-in
isDate()/isDateTime()methods onTextColumn
Requirements
- PHP 8.2+
- FilamentPHP 3.x, 4.x, or 5.x (single codebase across all three)
- Laravel 10, 11, or 12
License
MIT
