Installation
composer require spiriitlabs/form-filter-bundle
Add to config/bundles.php (Symfony) or config/packages/lexik_form_filter.yaml (Laravel via Symfony bridge):
SpiriitLabs\FormFilterBundle\SpiriitLabsFormFilterBundle::class => ['all' => true],
First Use Case: Filtering a Doctrine Query Define a filter DTO (Data Transfer Object) for your entity:
// src/Filter/ProductFilter.php
namespace App\Filter;
use SpiriitLabs\FormFilterBundle\Filter\FormFilterBuilder;
class ProductFilter
{
public ?string $name;
public ?int $minPrice;
public ?int $maxPrice;
}
Build the filter in a controller/service:
use SpiriitLabs\FormFilterBundle\Filter\FormFilterBuilder;
use Doctrine\ORM\EntityManagerInterface;
public function index(Request $request, EntityManagerInterface $em)
{
$filter = new ProductFilter();
FormFilterBuilder::build($request, $filter);
$queryBuilder = $em->getRepository(Product::class)->createQueryBuilder('p');
$queryBuilder = (new FormFilterBuilder())
->add('name', \Doctrine\ORM\Query\Expr\Comparison::LIKE, 'p.name')
->addRange('price', 'p.price', $filter->minPrice, $filter->maxPrice)
->build($queryBuilder);
$products = $queryBuilder->getQuery()->getResult();
return $this->render('product/index.html.twig', ['products' => $products]);
}
Key Files to Review
src/Filter/FormFilterBuilder.php (Core logic)src/DependencyInjection/ (Configuration)tests/ (Usage examples)*Filter suffix (e.g., UserFilter, OrderFilter).?string, ?int) for optional filters.FilterCollection or custom builders.
class UserFilter {
public ?string $name;
public ?FilterCollection $roles; // For multi-select filters
}
$qb = $em->getRepository(Product::class)->createQueryBuilder('p');
$qb = (new FormFilterBuilder())
->add('name', 'LIKE', 'p.name')
->addRange('price', 'p.price', $filter->minPrice, $filter->maxPrice)
->addDateRange('createdAt', 'p.createdAt', $filter->startDate, $filter->endDate)
->build($qb);
addJoin() for related entities.
->addJoin('p.category', 'c')
->add('category.name', 'LIKE', 'c.name')
$form = $this->createForm(ProductFilterType::class, $filter);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// Build query with validated data
}
class ProductFilterType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('name', TextType::class)
->add('minPrice', IntegerType::class, ['required' => false])
->add('maxPrice', IntegerType::class, ['required' => false]);
}
}
use Knp\Component\Pager\PaginatorInterface;
$paginator = $this->get('knp_paginator');
$results = $paginator->paginate(
$queryBuilder,
$request->query->getInt('page', 1),
10
);
$filter = new ProductFilter();
$json = json_decode($request->getContent(), true);
FormFilterBuilder::buildFromArray($json, $filter);
Service Container Binding Bind the builder to Laravel’s container for dependency injection:
// config/app.php
'bindings' => [
SpiriitLabs\FormFilterBundle\Filter\FormFilterBuilder::class => function ($app) {
return new \SpiriitLabs\FormFilterBundle\Filter\FormFilterBuilder();
},
];
Request Handling
Use Laravel’s Request facade or inject Request directly:
use Illuminate\Http\Request;
public function index(Request $request) {
$filter = new ProductFilter();
\SpiriitLabs\FormFilterBundle\Filter\FormFilterBuilder::build($request, $filter);
// ...
}
Eloquent Integration For Eloquent queries, wrap the builder in a helper:
// app/Helpers/FilterHelper.php
class FilterHelper {
public static function apply(Builder $query, array $filterData) {
$filter = new ProductFilter();
FormFilterBuilder::buildFromArray($filterData, $filter);
$builder = new FormFilterBuilder();
// Map Doctrine expressions to Eloquent
$query->where($builder->getConditions($filter));
}
}
Case Sensitivity in LIKE Queries
LIKE is case-sensitive by default. Use LOWER() for case-insensitive searches:
->add('name', 'LIKE', 'LOWER(p.name)')
Null Handling
null) are ignored by default. To include them:
->add('name', 'LIKE', 'p.name', true) // Force include even if null
if ($filter->minPrice !== null || $filter->maxPrice !== null) {
$qb->andWhere('p.price BETWEEN :min AND :max')
->setParameter('min', $filter->minPrice ?? 0)
->setParameter('max', $filter->maxPrice ?? PHP_INT_MAX);
}
Circular References
UserFilter referencing OrderFilter which references UserFilter). Use FilterCollection for complex hierarchies.Performance with ORM
N+1 queries. Use fetch="EAGER" or DQL JOIN explicitly:
->addJoin('p.category', 'c', 'WITH', 'c.id = p.categoryId')
Archived Status
Query Logging Enable Doctrine logging to inspect generated queries:
$em->getConnection()->getConfiguration()->setSQLLogger(new \Doctrine\DBAL\Logging\EchoSQLLogger());
Or use Laravel’s query logging:
\DB::enableQueryLog();
$queryBuilder->getQuery()->getSQL(); // Inspect the final query
Filter Validation
if ($filter->minPrice > $filter->maxPrice) {
throw new \InvalidArgumentException("Min price cannot exceed max price.");
}
Parameter Binding
$query = $queryBuilder->getQuery();
dump($query->getParameters());
FormFilterBuilder to add domain-specific filters:
class CustomFormFilterBuilder extends FormFilterBuilder {
public function addCustomCondition(string $field, string $operator, string $path, $value) {
// Custom logic (e.g., full-text search)
$this->conditions[] = $this->expr->func('MATCH_AGAINST', $path, ':value');
$this
How can I help you explore Laravel packages today?