bartlomiejbeta/filter-sorter-bundle
Installation
composer require bartlomiejbeta/filter-sorter-bundle
Register the bundle in config/bundles.php (Symfony 4+) or AppKernel.php (Symfony 3):
BartB\FilterSorterBundle\FilterSorterBundle::class => ['all' => true],
Extend Repository
Replace your base repository with AbstractEntitySpecificationAwareRepository:
use BartB\FilterSorterBundle\Repository\AbstractEntitySpecificationAwareRepository;
class CarRepository extends AbstractEntitySpecificationAwareRepository
{
public function __construct(EntityManagerInterface $em)
{
parent::__construct($em, $this->getEntityName());
}
protected function getEntityName(): string
{
return Car::class;
}
}
First Filter/Sort Endpoint Create a controller to handle filtered/sorted queries:
use BartB\FilterSorterBundle\Manager\FilterQueryManager;
use BartB\FilterSorterBundle\Converter\FilterParamConverter;
use BartB\FilterSorterBundle\Converter\SortParamConverter;
class CarController extends AbstractController
{
public function list(FilterQueryManager $filterQueryManager, FilterParamConverter $filterConverter, SortParamConverter $sortConverter)
{
$filter = $filterConverter->convert($this->get('request')->query->all());
$sort = $sortConverter->convert($this->get('request')->query->all());
$queryBuilder = $filterQueryManager->getQueryBuilder(
Car::class,
$filter,
$sort
);
return $this->json($queryBuilder->getQuery()->getResult());
}
}
Define a Specification
Create a simple specification for filtering (e.g., CarMakeSpecification):
use BartB\FilterSorterBundle\Specification\SpecificationInterface;
class CarMakeSpecification implements SpecificationInterface
{
private $make;
public function __construct(string $make)
{
$this->make = $make;
}
public function isSatisfiedBy(object $car): bool
{
return $car->getMake() === $this->make;
}
}
Register Filter/Sort Services
Configure filters/sorts in services.yaml:
services:
App\Filter\CarMakeFilter:
tags:
- { name: filter_sorter.filter, entity: App\Entity\Car, alias: make }
App\Sort\CarMakeSort:
tags:
- { name: filter_sorter.sort, entity: App\Entity\Car, alias: make }
First API Call Hit your endpoint with query params:
/api/cars?filter[make]=Toyota&sort[make]=asc
Filter Creation
CarMakeFilter, CarYearFilter).FilterParamConverter to parse query params (e.g., ?filter[make]=Toyota).class CarMakeFilter implements FilterInterface
{
public function getSpecification(array $params): SpecificationInterface
{
return new CarMakeSpecification($params['make']);
}
}
Sorting Integration
CarMakeSort, CarPriceSort).SortParamConverter to parse sort params (e.g., ?sort[make]=asc).class CarMakeSort implements SortInterface
{
public function getOrderBy(array $params): string
{
return $params['direction'] === 'desc' ? 'c.make DESC' : 'c.make ASC';
}
}
Query Construction
FilterQueryManager to combine filters and sorts:
$queryBuilder = $filterQueryManager->getQueryBuilder(
Car::class,
$filter,
$sort
);
SpecificationCollection:
$collection = new SpecificationCollection();
$collection->add($makeSpec, $yearSpec);
Pagination
$queryBuilder->setFirstResult($page * $limit)
->setMaxResults($limit);
Reusable Filter Groups
advanced, basic) in services.yaml:
tags:
- { name: filter_sorter.filter, entity: App\Entity\Car, alias: advanced, group: advanced_filters }
?filter_group=advanced).Symfony Forms
FilterParamConverter to bind query params to form data:
$filterData = $filterConverter->convert($request->query->all());
$form = $this->createForm(CarFilterType::class, $filterData);
Doctrine Extensions
Like, NotLike):
$queryBuilder->andWhere('c.name LIKE :name')
->setParameter('name', '%' . $filter['name'] . '%');
API Platform
getCollectionOperations to inject filtering/sorting:
public function getCollectionOperations(EntityManagerInterface $manager)
{
$collection = parent::getCollectionOperations($manager);
$collection['get']['filters'] = 'filter[make][operator][eq]';
return $collection;
}
Caching Queries
SpecificationCollection or query builder results:
$cacheKey = md5(serialize($filter) . serialize($sort));
if (!$cachedResult = $cache->get($cacheKey)) {
$result = $queryBuilder->getQuery()->getResult();
$cache->set($cacheKey, $result, 3600);
}
Validation
$validator = $this->get('validator');
$errors = $validator->validate($filter);
if (count($errors) > 0) {
throw new \InvalidArgumentException((string) $errors);
}
Deprecated Symfony 3 Syntax
AppKernel). For Symfony 4/5:
config/bundles.php instead of AppKernel.get() with dependency injection (e.g., FilterQueryManager $filterQueryManager).Query Builder Overrides
AbstractEntitySpecificationAwareRepository, ensure getEntityName() is implemented correctly. Misconfiguration leads to:
[Semantical Error] The entity name could not be found.
Specification Performance
isSatisfiedBy(). Use SpecificationCollection for chaining:
// Bad: Nested conditions in one spec.
public function isSatisfiedBy($car) {
return $car->getMake() === $make && $car->getYear() > 2010;
}
// Good: Split into separate specs.
Sorting Ambiguity
?sort[make]=asc may conflict with entity property names. Use aliases:
tags:
- { name: filter_sorter.sort, entity: App\Entity\Car, alias: manufacturer }
Then call ?sort[manufacturer]=asc.Case Sensitivity
make === 'Toyota') are case-sensitive by default. Normalize input:
$this->make = strtolower($make);
Circular Dependencies
FilterQueryManager into filter/sort services to prevent circular references.Query Dump
dump($queryBuilder->getQuery()->getSQL());
dump($queryBuilder->getQuery()->getParameters());
Specification Logging
isSatisfiedBy() to trace filtering:
error_log("Checking make: {$this->make} against car: " . $car->getMake());
ParamConverter Issues
FilterParamConverter fails silently, enable debug mode and check:
php bin/console debug:container filter_param_converter
Entity Not Found
How can I help you explore Laravel packages today?