symfony-twig/pager-bundle
symfony-twig/pager-bundle is a Symfony bundle for adding Twig-powered pagination to your projects. It provides reusable pager rendering and page navigation controls, helping you display paginated lists with configurable ranges and simple template integration.
First, resolve the framework boundary — everything else depends on it. symfony-twig/pager-bundle ships a Bundle class, a service definition, and Twig templates. It boots through Symfony's kernel and renders through Twig. In a Laravel app the PHP classes will autoload, but no service provider registers the pager, no Twig environment exists to call the helper, and the bundle's Resources/ templates never resolve. There is no php artisan vendor:publish path and no config file to fall back on. If the target codebase is Laravel, the day-to-day answer is: use LengthAwarePaginator / paginate() and skip this package entirely.
If (and only if) you maintain a Symfony ≥3 app, the first twenty minutes look like this:
composer.json before installing. Check type (symfony-bundle), the symfony/* constraint range, and the twig/twig range. A package pinned to Symfony 3 will drag in EOL constraints and trigger Composer security-advisory warnings on install — know that before you open the PR.config/bundles.php. Symfony 3: AppKernel::registerBundles().?page= query param, feed a count + a sliced result set into the pager, render the controls in a partial.A minimal Symfony first pass:
// Controller
$page = max(1, (int) $request->query->get('page', 1));
$perPage = 20;
$total = $repo->countAll($filters);
$items = $repo->findPage($filters, $offset = ($page - 1) * $perPage, $perPage);
$pager = $this->get('pager') // whatever the service id is in services.xml
->setTotal($total)
->setPage($page)
->setPerPage($perPage);
return $this->render('list.html.twig', ['items' => $items, 'pager' => $pager]);
Then in Twig, call the registered helper (name it from the extension class) and pass the incoming query string so filters and sort survive page navigation. Validate that query-string preservation works before you build anything else on top — it's the single most common reason a paginator gets ripped out mid-sprint.
If you're on Laravel and landed here anyway, the equivalent first pass is Model::query()->filter($filters)->paginate($perPage)->withQueryString() in the controller, {{ $items->links() }} in Blade, and a published pagination::tailwind (or custom) view if the design system needs different markup. You get all of this in the box.
Wrap it behind your own interface — the single most important pattern here. Given zero stars, zero dependents, and a single maintainer, treat the vendor call site as something you will replace. Define an internal contract and implement it with this bundle:
interface PaginatorInterface {
public function paginate(object $query, int $perPage, int $page): PageResult;
}
PageResult is a neutral DTO — items, total, perPage, currentPage, lastPage. Controllers and templates depend on PageResult, never on the bundle's pager class. When you migrate to Pagerfanta (or off Symfony entirely), one adapter file changes. Without this, the vendor type leaks into every controller and Twig template and the swap becomes a multi-day refactor.
Config-driven page size, with a hard ceiling. Don't scatter 20 and 50 through controllers. Put a default in parameters/services.yaml, inject it into the pager, and clamp anything user-supplied:
$perPage = min((int) $request->query->get('per_page', $default), $maxPageSize);
This is the difference between a paginator and a denial-of-service vector. If the bundle doesn't clamp for you, clamp in your adapter.
One shared Twig partial for all list views. Even if the bundle's helper is adequate, wrap the helper output in your own _pager.html.twig that includes the current filters, the empty state, and your design-system classes. Every list screen (include '_pager.html.twig' with {pager: pager}) then inherits a consistent contract — page-size selector, "showing X–Y of Z", ellipsis rules. This is the actual value of a shared pagination component; it is also something you can build in a morning, which is the point.
Preserve query state on every link. Merging the current request query (minus page) into generated URLs is non-negotiable for filterable/searchable lists. If the bundle's helper doesn't do it, pass app.request.query.all into the template and merge. Test with at least two active filters plus a sort.
Handle out-of-range pages deliberately. Decide once: clamp to lastPage, or 404/redirect. Deep-linking to ?page=9999 after a filter shrinks the result set is common; unpaginated behaviour (empty table with valid controls) confuses users and generates tickets.
Laravel equivalent, for parity of thinking. The native paginator already implements all of the above — paginate() reads ?page=, withQueryString() preserves filters, links() renders controls, onEachSide() tunes the window. The pattern to carry over is containment and a shared Blade view, not the bundle's API.
composer require will succeed; nothing will work. If a ticket or catalogue entry sent you here from a Laravel stack, close it and use the native paginator.spaceless removal, escaping defaults) that can break rendering with no test suite to catch it. Budget for a fork within twelve months of adoption.COUNT(*) is where mis-pagination hides. With GROUP BY, HAVING, DISTINCT, or to-many joins, a naive count returns the wrong total and you get duplicate or skipped rows across page boundaries. Count DISTINCT on the primary key, or count a subquery. Users notice this before your monitoring does.href attributes must be escaped consistently. An unaudited, template-driven paginator is exactly where ?q="><script> shows up. Verify escaping output manually on the first screen you build.int, clamp to >= 1, clamp perPage to a maximum, and never interpolate it into SQL. Offset arithmetic is ($page - 1) * $perPage — an unbounded perPage turns one request into a table scan.OFFSET doesn't scale, and this package doesn't address it. For large tables, keyset (cursor) pagination is the correct answer; keep offset pagination for admin screens and low-cardinality lists. Document the threshold where teams should switch.PaginatorInterface (page 0, page past end, empty set, exact multiple of page size) and run it on every framework upgrade. That test is your only regression net.vendor/. For anything the pager service doesn't support (custom ellipsis logic, AJAX links), fork into your own namespace under MIT — and assign an owner before you merge it.How can I help you explore Laravel packages today?