Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Doctrine Pagination Bundle Laravel Package

javihgil/doctrine-pagination-bundle

A Symfony bundle that adds simple pagination for Doctrine queries, helping you split ORM and DBAL results into pages with a reusable paginator service. Ideal for building paged listings, search results, and admin grids without writing custom limit/offset logic.

View on GitHub
Deep Wiki
Context7

Getting Started

The bundle is a thin wrapper over Doctrine\ORM\Tools\Pagination\Paginator: you hand it a QueryBuilder plus a page number and page size, and it hands back something you can iterate and query count() / getTotalPages() on. That's the whole mental model — don't expect filtering DSLs, sort whitelisting, or Twig widgets unless you add them.

1. Install and register

composer require javihgil/doctrine-pagination-bundle

This is a low-profile package (no Flex recipe), so add it to config/bundles.php manually unless it's already there:

JavihGil\DoctrinePaginationBundle\DoctrinePaginationBundle::class => ['all' => true],

2. Find the service definition before writing any code

The public API of a bundle this size is its service definition, not its README. Open vendor/javihgil/doctrine-pagination-bundle/Resources/config/ (or config/) and look at the XML/YAML. You want two things:

  • the service ID and whether it's public,
  • whether the class name is aliased for autowiring (older bundles often only register an ID like javihgil_doctrine_pagination.paginator, so __construct(Paginator $p) will fail without an alias).

If autowiring isn't set up, add the alias yourself in config/services.yaml:

services:
    JavihGil\DoctrinePaginationBundle\...\Paginator:
        alias: javihgil_doctrine_pagination.paginator
        public: true

3. First use case: paginate a repository query

// src/Repository/ProductRepository.php
public function findPage(int $page, int $perPage): Paginator
{
    $qb = $this->createQueryBuilder('p')
        ->leftJoin('p.category', 'c')->addSelect('c')
        ->orderBy('p.createdAt', 'DESC')
        ->addOrderBy('p.id', 'DESC');   // tiebreaker — see Gotchas

    return $this->paginator->paginate($qb, $page, $perPage);
}
// src/Controller/ProductController.php
#[Route('/products', name: 'product_index')]
public function index(Request $request, ProductRepository $products): Response
{
    $page    = max(1, $request->query->getInt('page', 1));
    $perPage = min(100, max(1, $request->query->getInt('per_page', 20)));

    return $this->render('product/index.html.twig', [
        'products' => $products->findPage($page, $perPage),
    ]);
}

Implementation Patterns

Repository-owns-pagination. Keep page/per-page parsing in the controller and the paginator call in the repository. Controllers stay thin, and every list endpoint gets the same clamping logic. If several repositories need it, put the paginate() helper on an abstract PaginatedRepository base class.

Two shapes of result. Almost every consumer needs only three things: the current slice (iterable), the total item count, and the total page count. Normalise them once so Twig and your API layer don't reach into the paginator differently:

final class Page implements \IteratorAggregate
{
    public function __construct(
        public readonly iterable $items,
        public readonly int $total,
        public readonly int $page,
        public readonly int $perPage,
    ) {}

    public function getIterator(): \Traversable { return new \ArrayIterator(iterator_to_array($this->items)); }
    public function lastPage(): int { return max(1, (int) ceil($this->total / $this->perPage)); }
}

JSON APIs. Iterate the paginator into your serializer and emit a meta block: page, per_page, total, last_page. Don't serialize the Paginator itself — it carries the Query and will blow up or leak SQL.

Twig partial. One _pagination.html.twig that takes the paginator plus the route name and renders prev/next and a windowed page range. Because this bundle doesn't ship a widget, this is your UI layer — build it once.

Query shaping tips that pay off daily

  • Fetch-join only to-one associations (->addSelect('c')); for to-many use a separate hydration step or setFetchJoinCollection(false).
  • Apply filters before handing the QueryBuilder over; the paginator will reuse the same Query for the count, so anything added after is ignored or diverges.
  • For read-heavy lists, a hand-written COUNT(*) over the same WHERE clause is often faster than the paginator's count query — swap it in if you expose a custom count.

Gotchas and Tips

Always order deterministically. Without a unique tiebreaker in ORDER BY, rows shuffle between pages as data changes, and you get duplicate/missing items. ORDER BY id at the end costs nothing and fixes a whole class of "the pagination is broken" bugs.

Joins + collections produce wrong totals. Doctrine's Paginator needs setUseOutputWalkers(false) (and sometimes setFetchJoinCollection(false)) when you join to-many associations; otherwise the COUNT query can be invalid or wildly expensive on MySQL/Postgres. The bundle may or may not expose these — if it returns the raw Doctrine paginator, call them yourself before iterating.

DISTINCT and fetch-joins don't mix cheaply. A fetch-joined collection forces output walkers, which means loading every row of the page into memory to count. For large result sets, paginate IDs first, then hydrate the page in a second query.

This package is effectively unmaintained. Last release Dec 2022, zero stars, zero dependents, no test suite. Treat it as a convenience wrapper you could replace in an afternoon: the fallback is Doctrine\ORM\Tools\Pagination\Paginator directly, and the migration is mechanical. Don't let its class names leak into domain code — wrap it behind your own Page/PaginatorInterface so a swap is a one-file change.

Check autowiring and service visibility first. Most "it doesn't work" reports on bundles like this are an ServiceNotFoundException. Run bin/console debug:container | grep -i paginat before debugging anything else.

Debug the generated SQL, not the PHP.

$qb->getQuery()->getSQL();   // the slice query
// count query = clone the QB, replace select with COUNT(...)

If the count is wrong, it's almost always a join fan-out or a missing GROUP BY, not the bundle.

Configuration quirks. Look for a default_per_page style parameter in the bundle's extension class — but don't rely on it; pass perPage explicitly from your own config so changing the bundle version can't silently change your page sizes.

Extension points are the standard Symfony three: decorate the paginator service to add metrics/logging, override any Twig template the bundle registers via twig.paths, and bind globals in config/packages/twig.yaml if you want the page size available in templates without threading it through every action.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console