aelfannir/doctrine-api-paginator
Symfony bundle for paginating Doctrine ORM queries with API-friendly filters. Supports property and nested compound filters (AND/OR) and operator-based comparisons to build query conditions cleanly for result lists.
Installation:
composer require aelfannir/doctrine-query-paginator
For non-Symfony Flex projects, ensure the bundle is enabled in config/bundles.php:
AElfannir\DoctrineQueryPaginator\DoctrineQueryPaginatorBundle::class => ['all' => true],
First Use Case: Inject the paginator service into your controller/service:
use AElfannir\DoctrineQueryPaginator\Paginator;
public function __construct(private Paginator $paginator) {}
Use it to paginate a Doctrine query:
$query = $entityManager->createQueryBuilder()
->select('u')
->from(User::class, 'u');
$paginatedResults = $this->paginator->paginate($query, $request->query->getInt('page', 1), 10);
Key Files to Review:
src/Paginator.php (core logic)src/Filter/FilterBuilder.php (filter construction)tests/ (usage examples)Basic Pagination:
$query = $em->createQueryBuilder()->select('e')->from(Entity::class, 'e');
$results = $paginator->paginate($query, $page, $limit);
Returns an array with:
data: paginated entitiestotal: total countpage: current pagelimit: items per pageFilter Integration:
$filter = (new FilterBuilder())
->add('name', '=', 'John')
->add('age', '>', 18)
->build();
$query = $em->createQueryBuilder()->select('e')->from(Entity::class, 'e');
$paginator->applyFilters($query, $filter);
API Response Helper:
$response = [
'data' => $results['data'],
'meta' => [
'total' => $results['total'],
'pages' => ceil($results['total'] / $results['limit']),
],
];
Dynamic Filtering:
$filter = (new FilterBuilder());
foreach ($request->query->all() as $key => $value) {
$filter->add($key, 'LIKE', "%{$value}%");
}
Nested Compound Filters:
$filter = (new FilterBuilder())
->add('status', '=', 'active')
->or()
->add('createdAt', '>', new \DateTime('-30 days'))
->build();
Integration with Symfony Serializer:
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
$normalized = $serializer->normalize($results['data'], null, [
AbstractNormalizer::IGNORED_ATTRIBUTES => ['id', 'createdAt'],
]);
QueryBuilder Compatibility:
->expr()-> for complex conditions if needed.Filter Operator Mismatch:
IN require arrays:
$filter->add('id', 'IN', [1, 2, 3]); // Correct
$filter->add('id', 'IN', '1,2,3'); // Fails
Case Sensitivity:
getClassMetadata()->getFieldNames() to verify.Performance:
applyFilters() are not counted in COUNT(*). Use getTotal() separately if needed:
$total = $paginator->getTotal($query, $filter);
Log Filters:
$filter = (new FilterBuilder())->add('name', '=', 'John')->build();
\Log::debug($filter->toArray()); // Inspect structure
Query Dump:
$query = $em->createQueryBuilder()->select('e')->from(Entity::class, 'e');
$paginator->applyFilters($query, $filter);
\Log::debug($query->getQuery()->getSQL()); // Raw SQL
Custom Operators:
Extend Filter/OperatorInterface and register via service:
# config/services.yaml
AElfannir\DoctrineQueryPaginator\Filter\Operator\CustomOperator:
tags: { name: doctrine_query_paginator.operator }
Response Transformers: Override the default response structure by binding a custom service:
$paginator->setResponseTransformer($customTransformer);
Filter Validation: Use Symfony Validator for dynamic filters:
use Symfony\Component\Validator\Constraints as Assert;
$filter->add('price', new Assert\GreaterThan(0));
Default Page/Limit: Override via DI:
$paginator->setDefaultPage(1);
$paginator->setDefaultLimit(20);
Bundle Auto-Configuration:
If using Symfony Flex, the bundle auto-registers services. For manual setups, ensure services.yaml includes:
AElfannir\DoctrineQueryPaginator\:
resource: '../vendor/aelfannir/doctrine-query-paginator/src/'
exclude: '../vendor/aelfannir/doctrine-query-paginator/src/{Entity,Migrations,Tests,Kernel.php}'
How can I help you explore Laravel packages today?