smilingplants/pager-bundle
smilingplants/pager-bundle provides lightweight pagination for PHP applications, helping split large result sets into manageable pages for lists, search results, and admin screens. It offers a simple pager API with configurable page sizes and minimal setup.
Reality check before you type composer require. smilingplants/pager-bundle is a Symfony 2 / Twig pager bundle, not a Laravel package. It registers through AppKernel::registerBundles(), ships Twig templates, and will almost certainly fail Composer resolution on PHP 8 / Laravel 10–11 because its composer.json pins symfony/* to a 2.x range. There is no service provider, no facade, no Blade view — installing it into a Laravel app is not a supported path.
What you can do day-to-day is treat the repo as a design reference and build the pager on Laravel's native primitives. That is the realistic "first use case."
Minimal steps (Laravel):
Resources/views/ — the pager markup (window logic, first/last/ellipsis, disabled states).Twig/ — the extension that builds page ranges and computes offset/limit.Resources/config/services.yml — service wiring, mostly to confirm how thin the abstraction really is.$posts = Post::query()
->when($request->q, fn ($q, $s) => $q->where('title', 'like', "%{$s}%"))
->orderByDesc('created_at')
->paginate(15)
->withQueryString();
{{ $posts->onEachSide(2)->links('vendor.pagination.pager') }}
If you genuinely must run this inside a Symfony 2 app, install with dev-master, register SmilingPlants\PagerBundle\SmilingPlantsPagerBundle in AppKernel, then read services.yml to find the pager service id (it is not guessable) before writing any controller code.
The pattern that matters: reimplement the intent, not the code.
The bundle's only durable value is its pager behavior — window sizing, ellipsis rules, edge-case handling for empty results and single-page results. That lives in a handful of lines of Twig and PHP and is trivially expressible in Blade + LengthAwarePaginator. Copy the interaction spec; discard the implementation.
Custom pager view (mirrors the bundle's markup contract).
{{-- resources/views/vendor/pagination/pager.blade.php --}}
@if ($paginator->hasPages())
<nav role="navigation" aria-label="Pagination">
<ul class="pager">
{{-- Previous --}}
@if ($paginator->onFirstPage())
<li class="disabled" aria-disabled="true"><span>«</span></li>
@else
<li><a href="{{ $paginator->previousPageUrl() }}" rel="prev">«</a></li>
@endif
{{-- Window --}}
@foreach ($elements as $element)
@if (is_string($element))
<li class="disabled"><span>{{ $element }}</span></li>
@endif
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<li class="active" aria-current="page"><span>{{ $page }}</span></li>
@else
<li><a href="{{ $url }}">{{ $page }}</a></li>
@endif
@endforeach
@endif
@endforeach
{{-- Next --}}
@if ($paginator->hasMorePages())
<li><a href="{{ $paginator->nextPageUrl() }}" rel="next">»</a></li>
@else
<li class="disabled" aria-disabled="true"><span>»</span></li>
@endif
</ul>
</nav>
@endif
Reusable page-window helper (when you need the bundle's offset/limit math outside Blade).
// app/Support/Pagination/PageWindow.php
namespace App\Support\Pagination;
final class PageWindow
{
public static function offset(int $page, int $perPage): int
{
return max(0, ($page - 1) * $perPage);
}
public static function range(int $page, int $perPage, int $total): array
{
if ($total === 0) {
return [0, 0, 0]; // offset, limit, lastPage
}
$last = (int) ceil($total / $perPage);
$page = min(max(1, $page), $last);
return [self::offset($page, $perPage), $perPage, $last];
}
}
Pattern: cursor pagination when tables grow.
The bundle only models offset paging, which degrades at scale. Laravel's cursorPaginate() is the correct upgrade and requires no pager markup changes if you keep the view abstract:
$posts = Post::orderByDesc('id')->cursorPaginate(15);
// in Blade: $posts->previousPageUrl() / $posts->nextPageUrl() still work
Pattern: multi-page contexts on one screen.
If a dashboard shows two independently paginated lists (e.g. Posts and Comments), give each its own query-string prefix instead of fighting a shared page param:
Post::paginate(10, ['*'], 'posts_page');
Comment::paginate(10, ['*'], 'comments_page');
The Symfony bundle would require you to instantiate two pager services and manually reconcile params — Laravel does this natively via the third argument.
Integration tip: keep the paginator out of the controller body.
If the pager logic is starting to leak into controllers (matching the Symfony bundle's $pager->setTotalItems(...)->setOffset(...) style), that's a signal to move to a scoped query builder or a PaginationRequest DTO. The bundle's imperative setters are a pattern to unlearn — Laravel's paginator returns the data and metadata together.
The package is Symfony 2-only — do not force it into Laravel.
composer require will fail against modern PHP/Symfony constraints. Do not use --ignore-platform-reqs to get past it. That defers an end-of-life Twig 1.x dependency (with published security advisories) into runtime, where a template-injection vector becomes your incident, not your build's.
Query string loss is the classic bug.
Laravel pagination drops existing filters by default. Every paginated query that reads from $request->query(...) needs ->withQueryString(). The Symfony bundle handles this inside its link builder; in Laravel it's a per-call opt-in. Add a test asserting filters survive the ?page=2 navigation.
$elements vs $paginator->url() — pick one.
$elements gives you the windowed list with "..." as a string element; iterating 1..lastPage manually gives you full control but ignores onEachSide(). Mixing them produces duplicate or mis-numbered links. Stick with $elements for standard cases.
Escaping semantics differ.
Twig's autoescape and Blade's {{ }} are conceptually similar but not identical when you build URLs from raw query strings. If you copy Symfony template output verbatim, ensure anything interpolated into href is passed through {{ }} (escaped) — not {!! !!} — and validate page params as integers before they reach the URL builder.
Edge cases to explicitly handle (the bundle covers these; Blade partials often don't):
<nav> entirely (@if ($paginator->hasPages())).ceil($total / $perPage) manually will render a broken "last" link. Use $paginator->lastPage(), not your own math.0 page params — cast with $request->query->getInt('page', 1) and clamp with max(1, ...).onEachSide() is cheap, but don't over-tune it.
Renders 2 * onEachSide + 1 windowed links plus ellipsis. Values above 3 produce wide pagers on mobile and defeat the "window" concept. The Symfony bundle's window size is a config default; in Laravel it's a fluent call — keep it a named constant in a view composer or config so every pager in the app stays consistent.
Debugging pagination issues — order of checks:
->total() before ->links().)withQueryString() present? (Check url()->current() inside the view.)LengthAwarePaginator is single-use data-bound.)$paginator->url($page) or raw string building? Raw strings are the usual culprit for dropped params.Extension points you should keep (even though the bundle's are unusable):
resources/views/vendor/pagination/ so styling is centralized.config/pagination.php for default per-page and onEachSide values, read via config() in view composers. This is the Laravel-native equivalent of the Symfony bundle's DI parameters and gives you one knob to change behavior app-wide.When to escalate to keyset pagination.
Offset paging (LIMIT ... OFFSET ...) scans and discards rows; performance collapses past ~100k rows. If a page in your app is slow because of pagination, no pager package — this one or Laravel's — fixes it. Move to cursorPaginate() at the query layer, and accept that the UI becomes prev/next-only. The Symfony bundle has no answer for this; that's a signal, not a limitation.
Ramp-up asymmetry is the hidden cost.
The concept of a pager is a 20-minute ramp for any PHP developer. This artifact requires reverse-engineering a dead Symfony 2 bundle's conventions — a day of friction that produces nothing reusable. Prefer the native path: ->links('vendor.pagination.pager') is immediately legible to any Laravel hire, and the maintenance burden stays inside the framework's upgrade surface, not yours.
How can I help you explore Laravel packages today?