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
Suggests
- ptplugins/filament-pikaday: Lightweight Pikaday date picker (no jQuery/moment.js). When installed, enable 'auto-filters.prefer_pikaday' to route date range filters through it.
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 [ '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 'inline_labels' => true, // Apply ->inlineLabel() to every auto-filter 'prefer_pikaday' => false, // Use Pikaday for date filters when installed ];
Optional: Pikaday date picker
Date range filters use Filament's native DatePicker by default. If you install the lightweight ptplugins/filament-pikaday package (no jQuery, no moment.js), you can route every auto-generated date filter through it instead โ set prefer_pikaday => true:
composer require ptplugins/filament-pikaday
// config/auto-filters.php 'prefer_pikaday' => true,
The flag is a no-op when Pikaday isn't installed โ auto-filters falls back to the native DatePicker automatically, so it's safe to enable in shared config.
inline_labels defaults to true because that's what pairs best with the recommended slide-over panel below. Set it to false if you keep the default Filament dropdown layout (the dropdown is narrow and inline labels can look cramped) or if you simply prefer stacked labels.
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.
By default the trait applies inlineLabel() to every form field it generates (label on the left of the input, single row per filter โ controlled by inline_labels in the config). That layout is tuned for a wide panel โ pair it with a slide-over on the Resource and you get a "Linear / Notion"-style filter sidebar like 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
When inline_labels is true (the default), 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.
Set inline_labels => false in the published config to skip all of the above and get stacked labels instead โ better suited to the default dropdown layout.
If you write a custom filter (
Filter::make()->form([...])outside this trait) and want it to match the inline style, 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
