spiriitlabs/form-filter-bundle
SpiriitLabs FormFilterBundle is a Symfony bundle that turns standard Symfony forms into reusable data filters. Map form fields to Doctrine query conditions, keep controllers clean, and quickly build search, listing, and admin pages with configurable filtering.
This release (v12.3.0) is a documentation-and-tooling release — no API changes, no deprecations. The two changes that matter day-to-day are a new VitePress documentation site and a new profiler panel that traces filter state persistence.
Before you write code, calibrate your expectations: spiriitlabs/form-filter-bundle is a Symfony bundle (the maintained continuation of the Lexik form-filter lineage). It builds Doctrine queries from Symfony Form data. In a pure Laravel app you cannot "install and go" — there is no service provider, no config/form-filter.php, no Blade/Eloquent integration. You leverage it in three realistic situations:
symfony/form for standalone form handling and wants declarative, reusable filter forms.spatie/laravel-query-builder.First steps for a Symfony codebase:
composer require spiriitlabs/form-filter-bundle
Then register the bundle in config/bundles.php (Symfony Flex handles this), confirm the bundle's YAML block exists under config/packages/, and open the new VitePress docs first — the docs rewrite in PR #54 is the canonical starting point and is now more reliable than blog posts referencing the old Lexik namespace.
Your first use case should be the smallest one: a single TextFilterType field bound to a form, applied to a QueryBuilder in a controller. Get one filter working end-to-end, then add operators, choices, and ranges. Do not start with a 12-field filter form — when something is silently ignored you will have no idea which field caused it.
Filter form + QueryBuilder updater. The core loop is: build a form out of filter types → take the submitted/valid data → hand the form and a QueryBuilder to the updater, which walks the form children and calls andWhere/orWhere for each populated filter. Keep this in a dedicated repository method or a small query-filter service rather than inline in the controller.
One filter type per field, not per column type. Reach for the specific sub-types (TextFilterType, ChoiceFilterType, NumberFilterType, DateFilterType, BooleanFilterType, SearchFilterType) instead of a generic type — they carry the right operators and the right "is empty" semantics.
Compose with Collection/range for multi-value filters. Range and collection filters are where this bundle earns its keep over manual when() chains: one form field maps to a BETWEEN or IN clause with correct parameter binding.
Persist filter state in the session/query string, then let the new profiler panel verify it. PR #53 ("trace filter state persistence") adds a data-collector panel showing how filter state was carried across requests. If you implement "remember my filters" behavior (session key or a serialized query string in the URL), open the profiler after a submit and confirm the restored state matches what you expect before debugging why the query looks wrong.
Extend by subclassing filter types. When you need a custom operator set or a bespoke apply_filter behavior, subclass the closest built-in filter type and override its options rather than branching inside a controller. This keeps the profiler output and the QueryBuilder updater on the happy path.
Integration tip for Laravel-only teams: if you adopt the standalone symfony/form route, wrap the bundle's filter form in a small adapter that converts your Laravel request into a Symfony Request and back. Isolate that shim in one class; everything else (the form definitions, the filter types) is plain PHP and testable in isolation.
composer.json for symfony/form and doctrine/orm constraints before assuming it drops into your stack.Lexik\Bundle\FormFilterBundle\... namespace. Copy-pasted snippets will fatal on autoload. Use the SpiriitLabs\FormFilterBundle namespaces shown in the new VitePress docs.WHERE field IS NULL or = ''. If a filter appears to "do nothing," it is usually the empty-value guard, not the operator. Verify with the Symfony profiler's Doctrine panel by comparing the generated DQL before/after.prod and that your dev cache is cleared after upgrading — data collectors frequently fail to appear until bin/console cache:clear.andWhere accumulation. Filter conditions are appended, not replaced. Combining filters with hand-written DQL that also calls andWhere makes SQL ordering unpredictable; keep manual clauses out of the same QueryBuilder until the filters are applied.dev to validate the toolchain.How can I help you explore Laravel packages today?