Filters
Filters add a declarative filter popover to a server-side table, inspired by
Filament and
EasyAdmin. Declare them in configureFilters(); the Stimulus controller renders
a funnel toggle (with an active-count badge) next to the search box, opening a
popover that holds the controls plus Apply filters and Reset actions.
The table reloads over AJAX when filters are applied, and each filter applies a
Doctrine condition server-side.
Filters target server-side (Doctrine) tables. They have no effect on client-side data tables in this version.
Quick start
use Doctrine\ORM\QueryBuilder;
use Pentiminax\UX\DataTables\Filter\{ChoiceFilter, DateRangeFilter, Filter, TernaryFilter, TextFilter};
use Pentiminax\UX\DataTables\Model\Filters;
public function configureFilters(Filters $filters): Filters
{
$nameFilter = TextFilter::new('name')->label('Name');
$statusFilter = ChoiceFilter::new('status')
->label('Status')
->options(['Draft' => 'draft', 'Published' => 'published']);
$verifiedFilter = TernaryFilter::new('verified')
->field('emailVerifiedAt')
->trueLabel('Verified')
->falseLabel('Not verified');
$createdAtFilter = DateRangeFilter::new('createdAt')
->label('Created');
$vipFilter = Filter::new('vip')
->label('VIP only')
->query(
fn (QueryBuilder $qb, mixed $value, string $alias) => $qb->andWhere("$alias.score > :vip")->setParameter('vip', 100)
)
return $filters
->add($nameFilter)
->add($statusFilter)
->add($verifiedFilter)
->add($createdAtFilter)
->add($vipFilter);
}
Conventions
Every filter is created with ::new(string $name). The name is the AJAX payload
key (filters[name]).
| Method | Description |
|---|---|
label(string) | Control label (defaults to a humanized name) |
field(string) | Doctrine field path (defaults to the name). Supports relations, e.g. author.name |
placeholder(string) | Control placeholder / empty option label |
query(Closure) | Override the default condition: fn (QueryBuilder $qb, mixed $value, string $alias) |
A query() closure fully replaces the default behaviour of the filter type.
Filter types
TextFilter
Case-insensitive LIKE %value% on the target field.
TextFilter::new('name')->label('Name');
ChoiceFilter
Exact match, or IN (...) when multiple() is enabled. options() accepts the
[label => value] convention, a list of BackedEnum cases, or a BackedEnum
class-string.
ChoiceFilter::new('status')->options(['Draft' => 'draft', 'Published' => 'published']);
ChoiceFilter::new('roles')->multiple()->options(Role::class);
TernaryFilter
Three states (all / true / false). By default the true state matches
field IS NOT NULL and the false state field IS NULL. Use values() to
compare against concrete values instead (e.g. a boolean column).
TernaryFilter::new('verified')->field('emailVerifiedAt')
->trueLabel('Verified')->falseLabel('Not verified');
TernaryFilter::new('active')->values(true, false);
DateRangeFilter
Two optional bounds (from / to) applied independently as >= and <=.
DateRangeFilter::new('createdAt')->label('Created');
Filter (generic)
A checkbox driven entirely by a query() closure, executed only when checked.
Filter::new('vip')->query(
fn (QueryBuilder $qb, mixed $value, string $alias) =>
$qb->andWhere("$alias.score > :vip")->setParameter('vip', 100)
);
How it works
configureFilters()builds aFilterscollection, serialized into the Stimulusviewpayload under thefilterskey.- The controller registers a custom DataTables
filtersfeature and places it in thelayout— by default intopEnd, right after the built-insearchbox. Clicking the funnel toggle opens the popover; applying merges the committed values intoajax.dataasfilters[name], preserving any existing static AJAX data. - On Apply filters (or Reset), the table reloads. Server-side,
DataTableRequest::filterscarries the submitted values and each filter applies its condition after the standard search/order chain, so both the filtered count and the page reflect the filters.
Empty or irrelevant values are ignored (no-op).
Localizing the filter bar
The filter bar chrome strings — the Filters toggle, the Reset and
Apply filters buttons, and the All placeholder of empty select filters —
are translated automatically based on the active locale. The bundle ships a
DataTables translation domain (English and French out of the box); add a
DataTables.<locale>.xlf catalog in your app to cover more languages.
Override any string per table with Filters::labels(). Values may be plain
strings or your own translation keys (resolved in the default domain):
protected function configureFilters(Filters $filters): void
{
$filters
->labels(
title: 'datatable.filters', // translation key or literal
reset: 'datatable.reset',
apply: 'datatable.apply',
all: 'datatable.all', // the "All" placeholder
)
->add(ChoiceFilter::new('status')->options(Status::class));
}
Any argument left out keeps the bundle’s built-in localized default. To change a
single placeholder for one filter only, use placeholder() on that filter — it
takes precedence over the shared all label.
Positioning the filter button
By default the funnel toggle sits in topEnd, just after the search box. To put
it elsewhere, reference the filters feature in the table layout with
Feature::FILTERS; the controller injects the live filter instance at that
position instead of appending it:
use Pentiminax\UX\DataTables\Enum\Feature;
$dataTable->layout([
'topStart' => [Feature::FILTERS, Feature::PAGE_LENGTH],
'topEnd' => Feature::SEARCH,
]);