Installation:
composer require bugloos/query-filter-bundle
Add the bundle to your config/bundles.php under Bugloos\QueryFilterBundle\QueryFilterBundle.
First Use Case:
Filter a Book entity by title via a query string or array:
use Bugloos\QueryFilterBundle\QueryFilter;
// In a Symfony controller
$queryFilter = new QueryFilter($this->getDoctrine()->getManager());
$books = $queryFilter->filter(
$this->getDoctrine()->getRepository('App\Entity\Book')->createQueryBuilder('b'),
$_GET['filter'] ?? []
)->getQuery()->getResult();
Key Files to Review:
src/QueryFilter.php: Core logic for filtering.tests/Fixtures/: Example database schema and relations (e.g., Book, Author, Publisher).src/Filter/: Custom filter types (e.g., LikeFilter, InFilter).Use the QueryFilter class to apply filters to Eloquent-like queries (Symfony Doctrine QueryBuilder):
$qb = $entityManager->getRepository('App\Entity\Book')->createQueryBuilder('b');
$filteredResults = $queryFilter->filter($qb, [
'title' => 'Laravel', // LIKE '%Laravel%'
'price' => ['>', 20], // Price > 20
])->getQuery()->getResult();
Filter nested relations without joins using dot notation:
$filters = [
'author.name' => 'John', // Filters Book.author.name
'publisher.city' => 'Paris', // Filters Book.publisher.city
];
$queryFilter->filter($qb, $filters);
Under the hood, the bundle uses DISTINCT and IN subqueries for performance.
Extend default filters (e.g., LikeFilter, InFilter) for domain-specific logic:
use Bugloos\QueryFilterBundle\Filter\AbstractFilter;
class CustomRangeFilter extends AbstractFilter {
public function apply($queryBuilder, $field, $value, $alias) {
if (is_array($value)) {
list($min, $max) = $value;
return $queryBuilder->andWhere("$alias.$field BETWEEN :min AND :max")
->setParameter('min', $min)
->setParameter('max', $max);
}
return $queryBuilder;
}
}
Register it in services.yaml:
services:
Bugloos\QueryFilterBundle\Filter\CustomRangeFilter:
tags: [query_filter.filter]
Parse query strings in Symfony controllers:
use Symfony\Component\HttpFoundation\Request;
public function index(Request $request) {
$filters = $request->query->get('filter', []);
$qb = $entityManager->getRepository('App\Entity\Book')->createQueryBuilder('b');
return $queryFilter->filter($qb, $filters)->getQuery()->getResult();
}
Example URL:
/api/books?filter[title]=Laravel&filter[price][>]=20
N+1: Use fetchJoin for eager-loaded relations when filtering.author.name) are indexed.Case Sensitivity:
Default LikeFilter is case-insensitive. For case-sensitive searches, override the filter:
$queryBuilder->andWhere("$alias.$field LIKE :val")
->setParameter('val', "%$value%");
Deep Relation Limits:
The bundle supports 2-level deep relations by default. For deeper nesting, extend QueryFilter or use raw SQL.
Reserved Keywords:
Field names like order, limit, or group may conflict with query builder methods. Use aliases:
$filters = ['book_order' => 'ASC']; // Instead of 'order'
Null Handling:
Filters with null values may break queries. Sanitize input:
$filters = array_filter($_GET['filter'] ?? [], fn($v) => $v !== 'null');
$queryFilter->filter($qb, $filters)->getQuery()->getSQL();
if (!array_key_exists('author.name', $filters)) {
throw new \InvalidArgumentException("Invalid filter: author.name");
}
Reusable Filter Configs:
Define filter schemas per entity (e.g., BookFilter class) to enforce allowed fields:
class BookFilter {
public static function allowedFields(): array {
return ['title', 'author.name', 'published_at'];
}
}
Caching Filters: Cache filtered results for static or rarely changing data:
$cacheKey = md5(serialize($filters));
$results = $cache->get($cacheKey, function() use ($qb, $filters) {
return $queryFilter->filter($qb, $filters)->getQuery()->getResult();
});
Testing:
Use the bundle’s test fixtures (tests/Fixtures/) to validate edge cases:
$this->assertCount(2, $queryFilter->filter($qb, ['author.name' => 'John'])->getQuery()->getResult());
Symfony Forms Integration: Bind filters to form fields for user-friendly input:
$form = $this->createFormBuilder()
->add('title', TextType::class, ['attr' => ['class' => 'filter-input']])
->getForm();
Then extract values from $request->request->get('filter').
Performance Profiling: Compare query execution times with/without filters using Symfony Profiler or Blackfire. Deep relations may add overhead.
How can I help you explore Laravel packages today?