ekrouzek/pagination-filters-bundle
Installation
composer require ekrouzek/pagination-filters-bundle
Enable the bundle in config/bundles.php if not using Symfony Flex:
Ekrouzek\PaginationFiltersBundle\PaginationFiltersBundle::class => ['all' => true],
First Use Case
Annotate a controller method with @QueryParam for pagination, filtering, and sorting:
use Ekrouzek\PaginationFiltersBundle\Attribute\QueryParam;
#[QueryParam(name: "page", requirements: "\d+", default: 1)]
#[QueryParam(name: "itemsPerPage", requirements: "\d+", default: 10)]
public function getCourses(ParamFetcher $paramFetcher) {
$page = $paramFetcher->get('page');
$itemsPerPage = $paramFetcher->get('itemsPerPage');
// Use $page and $itemsPerPage in your query logic
}
Key Files to Review
src/Attribute/QueryParam.php (for annotations)src/ParamFetcher.php (for fetching validated params)src/EventListener/ParamFetcherListener.php (for integration hooks)Annotate Controller Methods
Define expected query parameters with @QueryParam:
#[QueryParam(name: "filter[name]", default: "", description: "Filter by course name")]
#[QueryParam(name: "sort[field]", default: "createdAt", description: "Sort field")]
#[QueryParam(name: "sort[direction]", default: "desc", description: "Sort direction")]
Inject ParamFetcher
Fetch validated parameters in your method:
public function getCourses(ParamFetcher $paramFetcher) {
$filters = $paramFetcher->getAll(); // Get all filtered params
$nameFilter = $paramFetcher->get('filter[name]');
$sortField = $paramFetcher->get('sort[field]');
$sortDirection = $paramFetcher->get('sort[direction]');
}
Apply Filters/Sorting to Queries Use fetched values to build dynamic queries (e.g., with Doctrine):
$queryBuilder = $entityManager->getRepository(Course::class)->createQueryBuilder('c');
if ($nameFilter) {
$queryBuilder->andWhere('c.name LIKE :name')->setParameter('name', "%$nameFilter%");
}
$queryBuilder->orderBy("c.$sortField", $sortDirection);
Pagination Logic Implement pagination (e.g., with KnpPaginator or manual offset/limit):
$offset = ($page - 1) * $itemsPerPage;
$queryBuilder->setFirstResult($offset)->setMaxResults($itemsPerPage);
Return Filtered Response Serialize results with metadata (e.g., total items, current page):
$results = $queryBuilder->getQuery()->getResult();
return $this->json([
'data' => $results,
'pagination' => [
'page' => $page,
'itemsPerPage' => $itemsPerPage,
'totalItems' => $totalItems, // From COUNT query
],
]);
@Groups with @QueryParam to document API contracts.@ApiProperty to auto-generate API docs:
#[QueryParam(name: "filter[status]", default: "active")]
#[ApiProperty(description: "Filter courses by status (active/inactive)")]
#[QueryParam(name: "page", requirements: "\d+", default: 1)]
#[Assert\GreaterThanOrEqual(1)]
ParamFetcherListener to add custom logic (e.g., default filters):
// src/EventListener/CustomParamFetcherListener.php
public function onKernelRequest(RequestEvent $event) {
$request = $event->getRequest();
if ($request->get('_route') === 'course_list') {
$request->query->set('filter[status]', 'active');
}
}
Register in services.yaml:
services:
App\EventListener\CustomParamFetcherListener:
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
Parameter Naming Conflicts
filter[name] vs. name). Use nested keys (e.g., filter[name] vs. search[name]).filter_, search_, or sort_.Default Values Overridden
@QueryParam are set after validation. If requirements fail, defaults are ignored.null + custom logic:
$page = $paramFetcher->get('page', 1); // Fallback to 1 if null
Case-Sensitive Sorting
ASC vs. asc).$direction = strtoupper($paramFetcher->get('sort[direction]', 'DESC'));
Missing ParamFetcher Injection
ParamFetcher will bypass all filtering logic.Performance with Complex Filters
LIKE or IN clauses can bloat queries. Test with EXPLAIN in production.Log Raw Parameters
Dump ParamFetcher output to debug:
$this->logger->debug('Raw params:', $paramFetcher->getAll());
Validate Annotations
Ensure @QueryParam attributes are correctly placed on methods, not classes.
Check Event Listeners
If filters aren’t applied, verify ParamFetcherListener is registered in services.yaml:
services:
Ekrouzek\PaginationFiltersBundle\EventListener\ParamFetcherListener:
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
Symfony Profiler Use the Request tab in Symfony Profiler to inspect query parameters.
Custom ParamFetcher
Extend ParamFetcher to add logic (e.g., type casting):
class CustomParamFetcher extends ParamFetcher {
public function getInt(string $name, int $default = null): int {
return (int) $this->get($name, $default);
}
}
Override in services.yaml:
services:
App\Service\CustomParamFetcher:
decorates: 'ekrouzek_pagination_filters.param_fetcher'
Dynamic Filter Rules Use events to modify filter behavior:
// src/Event/FilterEvent.php
class FilterEvent {
public function addRule(string $name, callable $rule) { ... }
}
Dispatch in ParamFetcherListener:
$dispatcher->dispatch(new FilterEvent($paramFetcher));
API Platform Integration
Combine with @ApiFilter for automatic OpenAPI docs:
use ApiPlatform\Metadata\Operation;
use Ekrouzek\PaginationFiltersBundle\Attribute\QueryParam;
#[Operation(
queryParams: [
new QueryParam(name: "page", requirements: "\d+"),
new QueryParam(name: "filter[name]"),
]
)]
Caching Filtered Responses Cache responses with a key including filter params:
$cacheKey = 'courses_' . md5(serialize($paramFetcher->getAll()));
return $this->cache->get($cacheKey, function() use ($queryBuilder) {
return $queryBuilder->getQuery()->getResult();
});
How can I help you explore Laravel packages today?