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]).

MethodDescription
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

  1. configureFilters() builds a Filters collection, serialized into the Stimulus view payload under the filters key.
  2. The controller registers a custom DataTables filters feature and places it in the layout — by default in topEnd, right after the built-in search box. Clicking the funnel toggle opens the popover; applying merges the committed values into ajax.data as filters[name], preserving any existing static AJAX data.
  3. On Apply filters (or Reset), the table reloads. Server-side, DataTableRequest::filters carries 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,
]);