gpslab/pagination-bundle
Symfony bundle integrating the gpslab/pagination library: registers pagination services, Twig helpers, and configuration so you can paginate Doctrine queries, arrays, and custom data sources with reusable page objects and simple templates.
Read this first: it is a Symfony bundle, not a Laravel package. gpslab/pagination-bundle ships a Symfony DI extension, a Twig extension, and a container service named gpslab_pagination.paginator. None of that boots in Laravel. The bundle has ~11 stars, zero dependents, and its last release was March 2020 — treat it as a reference implementation, not a dependency.
What you actually consume from Laravel is its framework-agnostic core, gpslab/pagination, which the bundle wraps. That library gives you two plain objects: a Paginator (computes page math) and a Page (immutable value object holding number, total, per-page, offset/limit).
Minimal steps:
composer require gpslab/pagination
Then, as your first use case, replace a hand-rolled ?page= calculation in a controller:
use GPSLab\Pagination\Paginator;
Route::get('/posts', function (Request $request, Paginator $paginator) {
$perPage = 20;
$current = max(1, (int) $request->query('page', 1));
$total = Post::where('published', true)->count();
$page = $paginator->paginate($total, $current, $perPage);
$posts = Post::where('published', true)
->orderByDesc('id')
->offset($page->getOffset())
->limit($page->getLimit())
->get();
return view('posts.index', compact('posts', 'page'));
});
Where to look first in the repo:
Paginator.php / Page.php — the only classes you will realistically use; read them end to end, they are short.Resources/views/ (the Twig pagination template) — useful as a layout reference for the markup you will rewrite in Blade.DependencyInjection/Configuration.php — shows the only extension point the bundle exposes (the default template name).Resources/config/services.yaml — shows exactly how thin the bundle is; it is ~one service and one Twig function.If you are genuinely in a Symfony app, composer require gpslab/pagination-bundle, enable it in bundles.php, and autowire GPSLab\Pagination\Paginate into a controller. That is the intended path — and the reason the package is a poor fit for a Laravel codebase.
Pattern 1 — count-then-slice (the only pattern this library supports). Paginator is pure arithmetic; it never touches a data source. You are responsible for the COUNT(*) and the OFFSET/LIMIT. This is fine for admin tables under a few hundred thousand rows and a liability above that (see Gotchas).
Pattern 2 — wrap it in a service so controllers stay clean.
namespace App\Support;
use GPSLab\Pagination\Page;
use GPSLab\Pagination\Paginator;
use Illuminate\Database\Eloquent\Builder;
final class TablePaginator
{
public function __construct(private Paginator $paginator) {}
public function paginate(Builder $query, int $page, int $perPage = 25): array
{
$total = (clone $query)->toBase()->getCountForPagination();
$p = $this->paginator->paginate($total, max(1, $page), $perPage);
return [
'items' => $query->offset($p->getOffset())->limit($p->getLimit())->get(),
'page' => $p,
];
}
}
Note getCountForPagination() rather than count() — it keeps existing joins/group-bys cheaper and is what Laravel's own paginator uses internally.
Pattern 3 — bridge to Laravel's view layer. The bundle's Twig template is useless to you; keep the Page object but feed it into a Blade component, or convert to LengthAwarePaginator so $items->links() and appends() keep working for free:
$lara = new \Illuminate\Pagination\LengthAwarePaginator(
$items, $page->getTotal(), $page->getPerPage(), $page->getNumber(),
['path' => request()->url(), 'query' => request()->query()]
);
This conversion is the pragmatic move: you get the math object you wanted plus the Bootstrap/Tailwind views Laravel already ships.
Pattern 4 — JSON APIs. Serialize the Page metadata explicitly instead of relying on Eloquent's toArray(). A stable shape (current_page, per_page, total, last_page, from, to) matters more than which class produced it; clients that later move to cursorPaginate should not break.
Pattern 5 — Symfony (only if you are actually there). Autowire Paginate, call paginate($total, $page, $perPage) in the controller, and render with the Twig helper the bundle registers (a paginate_render(page, template) style function — verify the exact signature in Twig/). Configuration is a single key for the default template; extend by passing a custom template name rather than by subclassing.
Pattern 6 — extension via decoration. If you need clamping, page-size allowlists, or a different default, decorate Paginate in a service provider rather than forking. It is a one-method interface, which is the one genuinely nice property of this package.
composer require gpslab/pagination-bundle in Laravel. Nothing will auto-discover, and Symfony\Bundle\FrameworkBundle / Twig\Extension\AbstractExtension may not be installed. Pull in gpslab/pagination only.GPSLab\Pagination\Paginator and Illuminate\Pagination\Paginator share a short name; never import both unqualified in one file. Alias one at the top.^1.0 (or whatever tag you verify) and cover your usage with a test. Assume no upstream fixes.0, negative pages, and pages past the end are not clamped for you. Clamp on input (max(1, ...)) and treat an empty result on an over-large page as a normal empty state — or 404 it deliberately.paginate(0, 1, 20) — total pages is typically 1 or 0 depending on version, and a last_page of 0 will produce /page/0 links. Guard your view.per_page from the query string is an attack surface. An unrestricted ?per_page=100000 will happily LIMIT 100000. Allowlist sizes.OFFSET pagination on large tables you pay for the COUNT(*) plus a deep offset scan on every request. Cache the count for a minute, or switch to Laravel's cursorPaginate for infinite-scroll surfaces and keep this only for numbered admin tables.->paginate() macro; you own the count, orderBy, and offset/limit. Missing a deterministic orderBy with an offset produces duplicated rows across pages.$page->getTotalPages(), getNumber(), getOffset(), getLimit() at the boundary; nine out of ten pagination bugs are a wrong total (forgotten where) or an off-by-one page, both visible in that one dump.Paginate interface (swap/decorate it), and the bundle's single template config key. There is no event system, no driver interface, and no Blade/Twig renderer abstraction to hook into — budget accordingly if you were hoping for a pluggable renderer.Page value object in a shared domain layer, or you are porting a Symfony service. Otherwise Laravel's paginate()/cursorPaginate() plus LengthAwarePaginator covers the same ground with maintained code, Blade views included, and no 2020-era dependency.How can I help you explore Laravel packages today?