brandoriented/form-filter-bundle
Install the Bundle
composer require lexik/form-filter-bundle
Add to config/bundles.php:
return [
// ...
Lexik\Bundle\FormFilterBundle\LexikFormFilterBundle::class => ['all' => true],
];
Create a Filter Form Type
Extend AbstractType and use filter-specific field types (e.g., TextFilterType, DateFilterType).
Example:
use Lexik\Bundle\FormFilterBundle\Form\Filter\FormType;
use Lexik\Bundle\FormFilterBundle\Form\Filter\Type\TextFilterType;
class PostFilterType extends FormType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextFilterType::class, [
'label' => 'Title',
'required' => false,
'filter_parameter_name' => 'title', // Maps to query param
]);
}
}
Register the Form Type
Add to config/services.yaml:
services:
App\Form\Filter\PostFilterType:
tags: ['form.type']
First Use Case: Filtering in a Controller
use Lexik\Bundle\FormFilterBundle\Form\Filter\FormInterface;
use Lexik\Bundle\FormFilterBundle\Filter\FormFilter;
public function listAction(Request $request, FormFilter $filter)
{
$form = $this->createForm(PostFilterType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$queryBuilder = $this->getDoctrine()
->getRepository(Post::class)
->createQueryBuilder('p');
$filter->filter($queryBuilder, $form);
$results = $queryBuilder->getQuery()->getResult();
}
// ...
}
Filter Form Creation
Lexik\Bundle\FormFilterBundle\Form\Filter\Type\* filter types (e.g., TextFilterType, DateRangeFilterType, EntityFilterType).filter_parameter_name: Maps form field to query parameter (e.g., ?title=search).filter_field_name: Overrides default field name in the query.filter_type: Custom filter logic (e.g., contains, starts_with).Query Integration
FormFilter service into controllers/repositories.QueryBuilder:
$filter->filter($queryBuilder, $form);
$filter->filter($qb, $form, 'AND'); // Default: 'AND'
Reusable Filter Logic
abstract class BaseFilterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('field', $this->getFilterType(), [
'filter_parameter_name' => $options['parameter_name'],
]);
}
}
API/REST Integration
filter_parameter_name to map query strings (e.g., /posts?title=test).$builder->add('title', TextFilterType::class, [
'constraints' => [new Length(['max' => 100])],
]);
LOWER(p.title) LIKE :title).Lexik\Bundle\FormFilterBundle\Filter\AbstractFilter for bespoke logic.kernel.request to auto-apply filters:
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if ($request->isMethod('GET') && $request->attributes->has('filter_form')) {
$form = $this->createForm($request->attributes->get('filter_form'));
$form->handleRequest($request);
// Apply filters...
}
}
Parameter Name Conflicts
filter_parameter_name is unique to avoid query parameter collisions.$filter->getFilterParameters($form); // Inspect generated params
Case Sensitivity in DQL
filter_type: 'contains' for case-insensitive searches:
$builder->add('title', TextFilterType::class, [
'filter_type' => 'contains',
]);
filter_type: 'exact'.Nested Filters
EntityFilterType:
// Bad: Self-referential entity
$builder->add('author', EntityFilterType::class, [
'class' => Author::class,
'filter_parameter_name' => 'author',
]);
property option to filter by a specific field:
$builder->add('authorName', TextFilterType::class, [
'property' => 'author.name', // Filters Author.name
]);
Performance with Large Datasets
index_by to QueryBuilder to avoid memory issues:
$qb->indexBy('p', 'p.id');
DISTINCT for multi-field filters:
$qb->distinct();
Form Validation vs. Filter Logic
@Assert\NotBlank) runs before filtering.filter_constraints to validate filter values:
$builder->add('publishedAt', DateFilterType::class, [
'filter_constraints' => [
new Assert\GreaterThanOrEqual(['value' => new \DateTime('-1 year')]),
],
]);
Query Dump
Use Doctrine\DBAL\Logging\EchoSQLLogger to inspect generated SQL:
$qb->getEntityManager()->getConnection()->getConfiguration()->setSQLLogger(new EchoSQLLogger());
Filter Parameter Inspection
$parameters = $filter->getFilterParameters($form);
dump($parameters); // Shows raw filter params
Event Subscribers Debug filter application with a subscriber:
public function onFilterApplied(FilterEvent $event)
{
dump($event->getQueryBuilder()->getDQL());
}
Custom Filter Types
Extend AbstractFilter for new logic:
use Lexik\Bundle\FormFilterBundle\Filter\AbstractFilter;
class CustomFilter extends AbstractFilter
{
public function apply(QueryBuilder $qb, $field, $value, $alias, $operator)
{
$qb->andWhere("LOWER($alias.$field) = LOWER(:val)")
->setParameter('val', $value);
}
}
Register in services.yaml:
services:
App\Filter\CustomFilter:
tags: ['lexik_form_filter.filter']
Override Default Filter Types
Replace built-in types (e.g., TextFilterType) by redefining them in your bundle.
Dynamic Filter Fields
Use FormEvents::PRE_SET_DATA to add fields conditionally:
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$data = $event->getData();
if ($data->isAdmin()) {
$event->getForm()->add('adminOnlyField', TextFilterType::class);
}
});
Localization Translate filter labels/placeholders via Symfony’s translation system:
{{ form_label(form.title) }} {# Renders translated label #}
Default Filter Types The bundle ships with:
TextFilterType (LIKE)DateFilterType (BETWEEN)EntityFilterType (IN)BooleanFilterType (EQ)NumberFilterType (EQ, GT, LT)Parameter Prefix Add a prefix to all filter parameters:
# config/packages/lexik_form_filter.yaml
lexik_form_filter:
parameter_prefix: 'filter_'
Now ?title=test becomes `?
How can I help you explore Laravel packages today?