aelfannir/doctrine-query-paginator
Symfony bundle providing Doctrine query pagination with a flexible filter system. Supports property and compound (AND/OR, nested) filters and operator-based comparisons to build result sets from request-driven criteria.
Installation:
composer require aelfannir/doctrine-query-paginator
For Symfony Flex projects, this auto-registers the bundle. For non-Flex projects, manually add to config/bundles.php:
AElfannir\DoctrineQueryPaginator\DoctrineQueryPaginatorBundle::class => ['all' => true],
First Use Case: Inject the paginator service into a controller/service:
use AElfannir\DoctrineQueryPaginator\Paginator;
public function __construct(private Paginator $paginator) {}
public function index(Request $request) {
$queryBuilder = $this->entityManager->getRepository(Entity::class)->createQueryBuilder('e');
$filters = $this->buildFiltersFromRequest($request); // Custom logic
$paginator = $this->paginator->paginate($queryBuilder, $filters, $request->query->getInt('page', 1), 10);
return $this->render('index.html.twig', ['paginator' => $paginator]);
}
Key Files:
src/Paginator.php: Core logic for pagination and filtering.src/Filter/FilterBuilder.php: Helper for constructing filter objects.$queryBuilder = $entityManager->getRepository(User::class)->createQueryBuilder('u');
$filters = [
['property' => 'name', 'operator' => '=', 'value' => 'John'],
['property' => 'active', 'operator' => '!=', 'value' => false]
];
$paginator = $this->paginator->paginate($queryBuilder, $filters, $page, $perPage);
public function buildFilters(Request $request): array {
$filters = [];
if ($request->query->has('search')) {
$filters[] = [
'property' => 'name',
'operator' => 'LIKE',
'value' => '%'.$request->query->get('search').'%'
];
}
return $filters;
}
$filters = [
'AND' => [
['property' => 'status', 'operator' => '=', 'value' => 'active'],
'OR' => [
['property' => 'createdAt', 'operator' => '>', 'value' => new \DateTime('-7 days')],
['property' => 'updatedAt', 'operator' => '>', 'value' => new \DateTime('-1 day')]
]
]
];
Use the paginator in a form type to enable filtered searches:
$form = $this->createFormBuilder()
->add('search', TextType::class, ['required' => false])
->getForm();
Extend the Operator class to add domain-specific logic (e.g., CONTAINS for arrays):
use AElfannir\DoctrineQueryPaginator\Filter\Operator;
class ContainsOperator extends Operator {
public function apply($value, $field, QueryBuilder $qb, $alias) {
$qb->andWhere($alias.'.'.$field.' LIKE :value')
->setParameter('value', '%'.$value.'%');
}
}
QueryBuilder Modification:
QueryBuilder instance for unrelated queries after pagination.QueryBuilder if needed:
$clone = clone $queryBuilder;
$this->paginator->paginate($clone, $filters, $page, $perPage);
Operator Limitations:
IN for arrays requires custom logic).Operator class or use raw SQL via ExpressionBuilder.Performance with Complex Filters:
EXPLAIN in your database.DISTINCT in the QueryBuilder for large datasets:
$queryBuilder->distinct();
Case Sensitivity:
LIKE operators may behave differently across databases (e.g., MySQL vs. PostgreSQL collations).LOWER() in custom operators for case-insensitive searches:
$qb->andWhere("LOWER(".$alias.'.'.$field.') LIKE LOWER(:value)')
->setParameter('value', '%'.$value.'%');
Pagination with Joins:
COUNT(DISTINCT) or GROUP BY in subqueries.$queryBuilder->select('COUNT(DISTINCT u.id)')
->from('App\Entity\User', 'u')
->leftJoin('u.orders', 'o');
Log Generated SQL:
Enable Doctrine SQL logging in config/packages/doctrine.yaml:
doctrine:
dbal:
logging: true
profiling: true
Check logs for the final query structure.
Validate Filters:
Use FilterBuilder::validate() to ensure filters are correctly structured before passing to the paginator:
$this->paginator->getFilterBuilder()->validate($filters);
Test Edge Cases:
InvalidArgumentException).AND/OR nesting loops).Custom Filter Builders:
Override FilterBuilder to add domain-specific validation or transformation:
class CustomFilterBuilder extends FilterBuilder {
public function transform($filters) {
// Add custom logic (e.g., convert snake_case to camelCase)
return parent::transform($filters);
}
}
Pagination Metadata:
Extend the Paginator class to add custom metadata (e.g., filter summary):
class ExtendedPaginator extends Paginator {
public function paginate(QueryBuilder $qb, array $filters, int $page, int $limit) {
$result = parent::paginate($qb, $filters, $page, $limit);
$result->setFilterSummary($this->buildFilterSummary($filters));
return $result;
}
}
Integration with API Platform:
Use the paginator in ApiFilter or ApiResource classes to enable filtered pagination in GraphQL/REST APIs:
use AElfannir\DoctrineQueryPaginator\Filter\FilterBuilder;
public function applyToCollection(Collection $collection, QueryBuilder $qb, array $arguments) {
$filters = $this->filterBuilder->fromArguments($arguments['filters']);
$this->paginator->applyFilters($qb, $filters);
}
How can I help you explore Laravel packages today?