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

Ux Pagination Laravel Package

symfony/ux-pagination

Symfony UX Pagination provides Twig components and helpers for adding pagination to Symfony applications. It simplifies navigating paged datasets such as lists and search results, with customizable controls that fit your templates and design.

View on GitHub
Deep Wiki
Context7

Getting Started

Frame the target stack before you type composer require. This is a Symfony UX package, not a Laravel one. It depends on symfony/http-foundation, symfony/options-resolver, the Twig bridge, and Doctrine. Its UI tier is Twig components, not Blade. There is no Laravel service provider and no Laravel adapter. So "getting started" splits into two paths:

  • You maintain a Symfony service → adopt it there. Effort is genuinely small.
  • You are in a Laravel app → you can only use the PHP side (Pager + adapter) headless and throw away the rendering tier, which is the majority of the package's value. Laravel already ships paginate() / cursorPaginate() with Blade views. Do the cost/benefit before pulling anything in.

First steps (Symfony context)

  1. composer require symfony/ux-pagination — Flex auto-registers the bundle; no manual bundles.php edit in a standard app.
  2. Confirm Twig + Doctrine ORM are present and that your asset pipeline (AssetMapper or Encore) is wired — the package ships assets/, and PHP/asset version skew is the classic UX failure mode.
  3. Read these files first, in this order — the docs are thin, the source is the documentation:
    • src/Pager.php — the object you actually pass to your controller and template.
    • src/AdapterInterface.php — the seam you'd implement to paginate anything non-Doctrine.
    • src/Adapter/Doctrine/ — QueryBuilderAdapter (numbered) and the cursor adapter.
    • templates/components/ — the numbered and cursor Twig component templates you'll be overriding.
  4. First use case: numbered pagination of a Doctrine query. Build a query, wrap it in an adapter, wrap that in a Pager, render the component.
// Controller — illustrative shape; verify the exact constructor against src/Pager.php
$qb = $postRepository->createQueryBuilder('p')
    ->where('p.published = :published')
    ->setParameter('published', true)
    ->orderBy('p.createdAt', 'DESC')
    ->addOrderBy('p.id', 'DESC');           // tiebreaker — non-negotiable, see Gotchas

$pager = new Pager(
    adapter: new QueryBuilderAdapter($qb),
    pageSize: 20,
);
$pager->setPage($request->query->getInt('page', 1));

return $this->render('post/index.html.twig', ['pager' => $pager]);
{% for post in pager %}            {# Pager is iterable: yields the current page #}
    {{ include('post/_card.html.twig', { post: post }) }}
{% endfor %}

{{ component('Pagination', { pager: pager }) }}

That is the whole happy path. The first thing to confirm once it renders is that getNbResults() matches what you expect — see the count-query warning below.

Implementation Patterns

Keep HTTP out of the pagination object's construction. Repositories should return queries; a single injectable factory should turn (query, page, size) into a Pager. That gives you one place to change page size, one place to add sorting whitelists, and one place to log slow counts.

final class PaginatorFactory
{
    public function __construct(private int $defaultPageSize) {}

    public function paginate(QueryBuilder $qb, Request $request, ?int $size = null): Pager
    {
        $pager = new Pager(new QueryBuilderAdapter($qb), $size ?? $this->defaultPageSize);
        $pager->setPage(max(1, $request->query->getInt('page', 1)));

        return $pager;
    }
}

Drive defaultPageSize from configuration, not literals in controllers — you will want per-endpoint sizes (20 for lists, 5 for sidebars) without a code change.

Use the numbered adapter for CRUD screens, the cursor adapter for feeds. Numbered pagination gives you "page 4 of 37" — that's what admin tables and search results need. Cursor pagination gives you a stable nextCursor and no OFFSET scan — that's what infinite scroll, activity feeds, and any append-heavy table needs. Do not use one for the other's job.

Cursor pattern — return JSON, skip the Twig component entirely.

$cursor = $request->query->get('cursor');
$pager  = new Pager(new CursorAdapter($qb), pageSize: 50);
// $pager->getCurrentPageResults() === this batch
// $pager->getNextCursor()          === string for the next request (null at end)

return $this->json([
    'items'      => $pager->getCurrentPageResults(),
    'nextCursor' => $pager->getNextCursor()?->toString(),
]);

The client sends ?cursor=<opaque string> back. Treat the cursor as opaque — never parse or construct it by hand in JS.

Rendering and overrides. Override templates by placing files under templates/bundles/UXPaginationBundle/components/ in the app; the per-strategy files (numbered.html.twig, cursor.html.twig) let you restyle markup without touching the component class. For one-off layouts, pass a different template to the component rather than forking the bundle's template.

If you are bridging into Laravel anyway, implement AdapterInterface (~30 lines) over Illuminate\Database\Query\Builder and use only the Pager as a value object. Do this only if you specifically want the package's PaginationInterface contract in shared PHP code; otherwise Laravel's own paginator already covers every case here and you avoid running two pagination paradigms side by side.

Avoid paginating across row-multiplying joins. LEFT JOIN ... comments plus OFFSET produces duplicate or short pages because the page boundary is applied to joined rows, not entities. If you must join, paginate a subquery of IDs first, then hydrate entities by those IDs — or use DISTINCT and accept the extra sort cost.

Gotchas and Tips

  • The framework mismatch is the first-order gotcha. No Blade views, no LengthAwarePaginator compatibility, no Laravel service provider. Copy-pasting KnpPaginatorBundle tutorials will also misfire — the class namespaces and Twig component names are different even where the concepts overlap.
  • Cursor pagination over DATETIMETZ_MUTABLE columns changed behavior in v3.6.0 (#3903). That was a correctness fix, which means pre-3.6 output was wrong. If you paginate over timezone-aware datetime columns, upgrading will silently shift page boundaries: rows can reorder, appear, or vanish at page seams. Write a regression test that asserts boundary rows across a page transition before you bump the constraint.
  • Always add a unique tiebreaker to ORDER BY. ORDER BY created_at DESC alone means ties are ordered arbitrarily, and rows straddling a page boundary get duplicated or skipped. Add , id DESC to every paginated query. This is the single most common cause of "the pagination is broken" bug reports against any paginator.
  • getNbResults() issues a COUNT query. On large tables that dominates page latency. For cursor-based endpoints, prefer hasNextPage/getNextCursor semantics and don't compute a total at all. If you need a total and it's expensive, cache it or approximate it — don't call getNbResults() from the template on every request without knowing what it costs.
  • Deep offset pages are O(n). Page 5000 is slow by definition. If a screen can reach deep pages, it's a cursor screen.
  • Grep for defaultPriorityMethod before upgrading. v3.6.0 (#4079) removed it from the adapter iterator, labelled a bug fix. Any subclass or call site will fatal. It's a loud failure, so CI catches it — but only if the code path is exercised.
  • Asset/PHP version skew produces unstyled-but-not-broken pagination. After a version bump, check whether assets/ changed and re-publish if so (importmap vs Encore differ here). A wrong template override path fails the same silent way: the bundle falls back to its default template and you get correct markup with the wrong look. Verify the override actually took effect in the rendered HTML, not just by eyeballing it.
  • Decide the out-of-range page policy explicitly. ?page=99999 or a garbage cursor should be a 404 or a redirect to the last page — verify what the pager does rather than assuming, because the default is usually "empty result set," which looks like a bug to users and to crawlers.
  • Debugging checklist when a page looks wrong: dump getNbResults(), getNbPages(), getPage(), and count($pager->getCurrentPageResults()). If the last one differs from pageSize on a non-final page, your adapter or your ORDER BY is the problem, not the template. For Doctrine, inspect $qb->getQuery()->getSQL() to confirm the LIMIT/OFFSET land where you expect.
  • Extension points are narrow but sufficient: AdapterInterface (custom data sources), Twig template overrides (presentation), and the Pager object itself (which is iterable, so a plain foreach in Twig works without any helper). There is no plugin system, no event system, and no query-optimization layer — if you need those, you're writing them.
  • Record the support horizon if you adopt this net-new. The package's lifetime tracks the Symfony release cycle, so it inherits an upgrade obligation tied to a framework your team may not otherwise track. For a Laravel-first team, that ongoing cost is the real argument against it — Laravel's cursorPaginate() already covers the cursor use case with zero new dependencies.
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.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route