ideup/simple-paginator-bundle
A lightweight Symfony bundle for adding simple, reusable pagination to your projects. It helps split Doctrine results or arrays into pages and render navigation controls, keeping templates clean and configuration minimal. A focused option for basic listing pages that don't need a heavier pagination...
Reality check before any commands: this is a Symfony 2 bundle, not a Laravel package. composer require ideup/simple-paginator-bundle inside a Laravel app will try to pull Symfony 2 components and collide with your framework. There is no service provider, no facade, no config/paginator.php. For a Laravel developer, "day-to-day leverage" means reading ~200–300 lines of source and porting the slice-and-count pattern into your own code — not adding a dependency.
Reading order (an hour, once):
Resources/config/services.yml — the service id (typically ideup.simple_paginator) and the class it wires. That class is the entire public API.Paginator value object — look for getItems(), getPage(), getTotalItems(), getTotalPages(), hasNextPage(), getNextPage(). These are the methods your port must replicate.Resources/views/ — the shipped Twig template showing exactly which variables the pager expects.Tests/ — for an unmaintained bundle, tests are the most accurate docs.First use case (the canonical Symfony 2 flow):
public function indexAction(Request $request)
{
$qb = $this->getDoctrine()->getRepository('AcmeDemoBundle:Post')
->createQueryBuilder('p')
->where('p.published = :published')
->setParameter('published', true)
->orderBy('p.createdAt', 'DESC');
$page = max(1, (int) $request->query->get('page', 1));
$result = $this->get('ideup.simple_paginator')->paginate($qb, $page, 20);
return $this->render('AcmeDemoBundle:Post:index.html.twig', array(
'items' => $result->getItems(),
'paginator' => $result,
));
}
First use case (what you would actually do in Laravel): reimplement that method over Illuminate\Database. One service class, one method, no dependency:
class PaginateQuery
{
public function __invoke($query, int $page = 1, int $perPage = 20): PaginatedResult
{
$page = max(1, $page);
$total = (clone $query)->count();
$items = $query->forPage($page, $perPage)->get();
return new PaginatedResult($items, $total, $page, $perPage);
}
}
Symfony 2 controller pattern. Inject the paginator service, pass a Doctrine QueryBuilder (or Query, or a plain array) plus page and page size. The bundle slices the result and returns a value object carrying items, current page, and page count. Keep controllers thin — pass the query builder, not a hydrated collection, so the count stays cheap.
Array pagination. The bundle also accepts plain arrays, useful for aggregating non-DB data before paging. In a port, collect($items)->forPage($page, $perPage) is the direct equivalent.
Configuration-driven page size and param name. Page size and the query parameter name (page by default) are configurable through the bundle's DI tree. Don't hardcode $_GET['page'] in templates — read the paginator's own getPage() so the link builder stays consistent.
Pagination behind an interface (the pattern worth stealing). Whichever stack you're on, do not let the paginator leak into application/domain code. Define your own contract and adapt:
interface PaginatesResults
{
public function paginate($query, int $page, int $perPage): PaginatedResult;
}
Then the Symfony bundle is one implementation, your Eloquent version is another, and swapping is a container binding — the real payoff for a package with zero dependents and a dead framework target.
Blade/template rendering. The shipped Twig template is disposable — copy the shape (previous, windowed page numbers, next), not the file. In Blade:
@foreach ($paginator->window() as $n)
<a href="{{ request()->fullUrlWithQuery(['page' => $n]) }}"
class="{{ $n === $paginator->currentPage() ? 'active' : '' }}">{{ $n }}</a>
@endforeach
Use fullUrlWithQuery() so filters/search survive page changes — a bug the 2013-era template won't guard against.
Porting to Eloquent. Replace Doctrine query decoration with Illuminate\Database equivalents: clone the query before count() (Eloquent builders are mutable and count() can strip select/orderBy assumptions), then forPage($page, $perPage)->get(). Keep the returned value object's method names close to the original to preserve mental mapping during migration.
Integration tips. Wrap the count in a cache for expensive filtered queries. Prefer the native Illuminate\Pagination for anything new — it already provides length-aware pagination, cursor pagination, and Blade views; the only reason to hand-roll is reproducing a specific pager UX.
getItems() vs getResult() exists across versions of this bundle. Don't write blind against a tutorial; check the actual class in services.yml's target first.page; passing 0 or negatives produces a bad offset. Always max(1, (int) $request->query->get('page', 1)) before calling, and clamp to the last page in your result object to avoid empty "page 99" screens.offset() by hand and you get off-by-one bugs that only show on the last page. Centralise the conversion in the result object.COUNT(*) cost. Naive total counting dominates response time on large filtered tables. Cache it per filter set, or skip total-based pagination for large datasets — the bundle has no concept of "length unaware" paging.OFFSET degradation. Offset paging skips rows under concurrent writes and slows at high page numbers. For big/fast-changing tables, the right answer is keyset pagination — a capability this bundle does not offer, which is itself a reason to use the native Laravel paginator instead of porting it.var_dump($page, $perPage, ($page - 1) * $perPage, $total). Nine times out of ten the arithmetic at the call site, not the bundle, is the bug.How can I help you explore Laravel packages today?