Symfony bundle for paginating arrays, Doctrine ORM repositories, and Doctrine ORM queries. Ships with a type-based pagination factory, built-in paginators, and Twig rendering helpers.
- Type-based pagination —
PaginationType(basic next/prev),RangeType(numbered page links with surrounding range), andExtendedPaginationType(next/prev with total counts) - Cursor-based pagination —
CursorTypefor keyset pagination using a single cursor value with direction derived from QueryBuilderorderBy - Auto-resolved cursor fields — ULID entities automatically resolve
cursor_fieldandcursor_getterfrom Doctrine metadata - Built-in paginators —
ArrayPaginator,EntityRepositoryPaginator,QueryPaginator, andCursorQueryPaginator - Extended pagination — optional total element count and page count computation for API metadata
- Twig integration —
render_pagination()function with an overridable sliding template - Repository trait —
PaginationEntityRepositoryTraitaddslist/listByhelpers to Doctrine repositories - Autowiring support — all services are auto-configured and tagged via Symfony DI
composer require chamber-orchestra/pagination-bundleIf you are not using Symfony Flex, register the bundle manually:
// config/bundles.php
return [
ChamberOrchestra\PaginationBundle\ChamberOrchestraPaginationBundle::class => ['all' => true],
];| Package | Purpose |
|---|---|
doctrine/orm + doctrine/doctrine-bundle |
Doctrine ORM pagination |
symfony/uid |
Auto-resolution of ULID cursor fields |
twig/twig |
Twig pagination rendering |
use ChamberOrchestra\PaginationBundle\Paging;
use ChamberOrchestra\PaginationBundle\Pagination\PaginationFactory;
final class BookController
{
public function __construct(
private Paging $paging,
private PaginationFactory $paginationFactory,
) {
}
public function index(): array
{
$pagination = $this->paginationFactory->create('range', [
'page' => 1,
'limit' => 10,
'extended' => true,
]);
$items = ['a', 'b', 'c'];
$result = $this->paging->paginate($items, $pagination);
return [
'data' => $result,
'meta' => $pagination->createView()->vars,
];
}
}use ChamberOrchestra\PaginationBundle\Paging;
use ChamberOrchestra\PaginationBundle\Pagination\PaginationFactory;
use Doctrine\ORM\EntityRepository;
public function list(EntityRepository $repository, Paging $paging, PaginationFactory $factory): array
{
$pagination = $factory->create('range', [
'page' => 1,
'limit' => 20,
'extended' => true,
]);
$items = $paging->paginate($repository, $pagination, [
'criteria' => ['status' => 'active'],
'orderBy' => ['id' => 'ASC'],
]);
return iterator_to_array($items);
}use ChamberOrchestra\PaginationBundle\Paging;
use ChamberOrchestra\PaginationBundle\Pagination\PaginationFactory;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\Book;
public function list(EntityManagerInterface $em, Paging $paging, PaginationFactory $factory): array
{
$query = $em->createQueryBuilder()
->select('b')
->from(Book::class, 'b')
->orderBy('b.id', 'ASC')
->getQuery();
$pagination = $factory->create('range', [
'page' => 2,
'limit' => 10,
'extended' => true,
]);
$items = $paging->paginate($query, $pagination);
return iterator_to_array($items);
}Cursor pagination uses a single cursor value instead of page numbers, providing stable results and efficient queries for large datasets. The pagination direction (forward/backward) is derived from the QueryBuilder's orderBy clause.
use ChamberOrchestra\PaginationBundle\Paging;
use ChamberOrchestra\PaginationBundle\Pagination\PaginationFactory;
use ChamberOrchestra\PaginationBundle\Pagination\Type\CursorType;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\Book;
public function list(
Request $request,
EntityManagerInterface $em,
Paging $paging,
PaginationFactory $factory,
): array {
$pagination = $factory->create(CursorType::class, [
'cursor' => $request->query->get('cursor'),
'limit' => 20,
]);
$qb = $em->createQueryBuilder()
->select('b')
->from(Book::class, 'b')
->orderBy('b.id', 'ASC');
$result = $paging->paginate($qb, $pagination, [
'cursor_field' => 'b.id',
'cursor_getter' => static fn (Book $book): mixed => $book->getId(),
]);
return [
'data' => $result,
'meta' => $pagination->createView()->vars,
// {
// "cursor": "42",
// "limit": 20,
// "next": "62",
// "previous": "43"
// }
];
}Auto-resolved cursor fields (ULID entities) — for entities with a ULID primary key, cursor_field and cursor_getter are auto-resolved from Doctrine metadata. No options needed:
// Entity with #[ORM\Column(type: 'ulid')] identifier — just pass the QueryBuilder
$result = $paging->paginate($qb, $pagination);This is handled by CursorFieldPaging, a decorator around Paging that is automatically registered when doctrine/orm is available. It inspects the QueryBuilder's root entity metadata and resolves the ULID identifier field and getter.
Reading cursor from request automatically — when the cursor option is omitted, CursorType reads it from the cursor request query parameter:
// GET /books?cursor=42
$pagination = $factory->create(CursorType::class, [
'limit' => 20,
// 'cursor' is read from ?cursor= automatically
]);Cursor presence indicates page availability — getNextCursor() returns null when there is no next page, and a cursor string when there is. Same for getPreviousCursor().
{{ render_pagination(pagination_view) }}Default templates are in src/Resources/views/ and can be overridden in your application.
| Type | Description | View vars |
|---|---|---|
pagination |
Basic next/previous navigation | current, startPage, previous, next |
range |
Numbered page links with configurable range | current, pagesCount, elementsCount, startPage, endPage, previous, next, pages, pageParameter, limit |
ExtendedPaginationType |
Next/previous with total counts | current, previous, next, pagesCount, elementsCount |
CursorType |
Cursor-based (keyset) pagination | cursor, limit, next, previous |
The pagination, range, and ExtendedPaginationType types accept page, limit (default 12), page_parameter, and extended options. The range type additionally accepts page_range (default 8).
The CursorType accepts cursor (?string), and limit (int, default 12). It requires a QueryBuilder target with an orderBy clause, and the cursor_field + cursor_getter (\Closure) paginator options (auto-resolved for ULID entities).
composer install
composer test # PHPUnit
composer analyse # PHPStan (level max)
composer cs-check # PHP-CS-Fixer (dry-run)MIT