ttskch/paginator-bundle
A lightweight Symfony bundle for simple, flexible pagination. Turn Doctrine queries or arrays into paginated results with sortable pages, configurable page sizes, and Twig-friendly output for building list screens quickly.
Framework reality check first: this is a Symfony bundle, not a Laravel package. There is no service provider, no facade, no Blade views, and no composer require path into a Laravel app that doesn't drag in symfony/http-foundation, Twig, and Doctrine. As a Laravel developer, your day-to-day default remains Model::paginate() / LengthAwarePaginator. Read this package only if you're maintaining a Symfony app, auditing a dependency, or mining it for a clean pagination design to port.
If you are in a Symfony codebase, the whole API is three classes — read them before the docs, because that's genuinely all there is:
src/Entity/Criteria.php — the request state (page, limit, sort key, direction).src/Paginator.php — paginate($target, $criteria): Slice.src/Entity/Slice.php (and its pages object) — what your template consumes.Minimal wiring:
composer require ttskch/paginator-bundle
# config/packages/ttskch_paginator.yaml
ttskch_paginator:
page_parameter_name: page
sort_parameter_name: sort
direction_parameter_name: direction
default_sort_key: id
default_direction: asc
sortable_fields: [id, name, createdAt]
limit: 10
max_limit: 100
template: '@TtskchPaginator/pagination.html.twig'
public function index(Request $request, Paginator $paginator, UserRepository $users): Response
{
$criteria = new Criteria(); // hydrate page/sort/direction from $request->query
$qb = $users->createQueryBuilder('u');
return $this->render('user/index.html.twig', [
'slice' => $paginator->paginate($qb, $criteria),
]);
}
First use case: a Doctrine-backed admin list with clickable column sorting. Confirm the exact Criteria hydration call against the README — the bundle is small enough that the source is more trustworthy than any tutorial.
Target types drive everything. paginate() accepts a Doctrine QueryBuilder (count + limit/offset pushed to SQL), a plain array (sliced in memory), or a callable returning items plus a total. Pick by data size: arrays only for already-materialized small sets.
Whitelist sorting, always. sortable_fields is your SQL-injection and "sort by a column that doesn't exist" guard. Treat an empty list as a bug, not a default — decide explicitly whether that means "no sorting" or "unrestricted."
Criteria as a GET form. The clean pattern is a small form type bound to Criteria with method="GET", so filters, sort, and page all round-trip through the URL and the listing stays bookmarkable. On any filter or sort change, force page = 1 — otherwise the user lands on page 7 of a two-page result.
Template override, not template fork. Point template at your own Twig file and keep the bundle's expected slice contract. This is the intended customization seam and keeps you off the upgrade path.
Adapter if you're porting. If the goal is the design rather than the bundle, lift two ideas into Laravel: a Criteria value object hydrated from the request, and a Slice/Pages object rendered by a Blade component. That maps almost one-to-one onto LengthAwarePaginator plus ->appends($request->query()), and you keep Laravel's onEachSide, withQueryString(), and links().
Where it integrates. Symfony controllers, Twig templates, Doctrine repositories. There is no Livewire/Inertia equivalent, no JSON pagination resource, no API-mode response shape — you build the envelope yourself.
max_limit silently clamps. If a request asks for limit=500 and max_limit is 100, you get 100 — and the page count changes underneath a user who bookmarked a deep page. Reflect the clamp in the UI, don't let it surprise people.COUNT(*) over a query with JOIN/GROUP BY overcounts rows, and your last page is empty. If the slice has fewer items than expected on the final page, this is why — inspect the generated count SQL.SELECT COUNT to a hot path without measuring.page, sort, direction are all renameable. If you're also running Laravel's ?page= anywhere nearby, or an API client assumes ?page[N], mismatches produce "always page 1" symptoms that look like a broken paginator.config/bundles.php if Flex doesn't. In Laravel there is nothing to register because there is nothing to run.Slice.Slice first, not the template. If current, last, total, and items|length disagree, the problem is the target/count, never the Twig.How can I help you explore Laravel packages today?