jasongrimes/paginator
A lightweight, framework-agnostic PHP pagination library that turns arrays or query results into page links. It calculates page ranges, handles edge cases, and renders customizable, accessible pagination markup—a simple drop-in for Laravel or any PHP app needing clean pagination.
The package is framework-agnostic, so there is no service provider or facade to register. Install it with Composer and instantiate it directly wherever you need pagination markup.
composer require jasongrimes/paginator
The core object takes four arguments and does exactly one job: it produces pagination chrome (the page numbers, ellipses, prev/next links). It does not fetch or slice your data — you still run your own skip()/take() query.
use JasonGrimes\Paginator;
$totalItems = Post::count();
$perPage = 15;
$currentPage = (int) request('page', 1);
$urlPattern = route('posts.index', ['page' => '(:num)']); // e.g. /posts?page=(:num)
$paginator = new Paginator($totalItems, $perPage, $currentPage, $urlPattern);
return view('posts.index', [
'posts' => Post::latest()->forPage($currentPage, $perPage)->get(),
'paginator' => $paginator,
]);
Where to look first: Paginator::getPages() — it returns the array of page/ellipsis/prev/next entries you can loop over — and Paginator::__toString(), which renders the bundled Stack Overflow–style template.
First use case: dropping {!! $paginator !!} into a Blade view instead of {{ $posts->links() }} when a designer wants the Flickr/Stack Overflow "1 2 3 … 47 48" layout with first and last pages always visible.
Avoid duplicating pagination math. Pull total/current/per-page from Laravel's paginator and feed them into this one:
$posts = Post::paginate(15);
$paginator = new Paginator(
$posts->total(),
$posts->perPage(),
$posts->currentPage(),
$posts->url(1) // returns a URL with page=1; swap in (:num) below
);
// Rebuild the pattern so the library can substitute page numbers
$urlPattern = $posts->url(1);
$paginator = new Paginator($posts->total(), $posts->perPage(), $posts->currentPage(),
str_replace('page=1', 'page=(:num)', $urlPattern));
This gives you Laravel's query handling plus this package's markup.
getPages() for full controlFor clean Blade output (with proper escaping), skip __toString() and loop the page objects:
@if ($paginator->getTotalPages() > 1)
<nav aria-label="Pagination">
<ul class="pagination">
@foreach ($paginator->getPages() as $page)
@if ($page->isDisabled)
<li class="disabled"><span>{!! $page !!}</span></li>
@elseif ($page->isCurrent)
<li class="active"><span>{{ $page->num }}</span></li>
@else
<li><a href="{{ $page->url }}">{{ $page->num }}</a></li>
@endif
@endforeach
</ul>
</nav>
@endif
Each page object exposes num, url, isCurrent and isDisabled, and its __toString() outputs the inner label («, », …, or the number).
If you want to keep {!! $paginator !!} but match Tailwind or Bootstrap 5, override the template once via a small view composer or a base controller:
$paginator->setTemplate(<<<'HTML'
<ul class="flex gap-1">
<li>{{prev}}</li>
{{pages}}
<li>{{next}}</li>
</ul>
HTML);
The package's placeholders are {{pages}}, {{prev}}, {{next}}, {{first}}, {{last}}, {{current}}, {{total}}. Do not feed it a compiled Blade view here — the {{ }} syntax will collide.
Since every list screen needs this, register a composer that injects a configured paginator:
View::composer('partials.pager', function ($view) {
$view->with('pager', new Paginator(
request()->attributes->get('total', 0),
request()->attributes->get('per_page', 15),
(int) request('page', 1),
request()->fullUrlWithQuery(['page' => '(:num)'])
));
});
fullUrlWithQuery is the cleanest way to preserve existing filters/sorts in the URL pattern.
Any countable list works — API result envelopes, log files, array keys:
$paginator = new Paginator(count($results), 25, $page, '/logs?page=(:num)');
$slice = array_slice($results, ($page - 1) * 25, 25);
forPage()/array_slice(). Only the links are generated.(:num) is mandatory in the URL pattern. If the placeholder is missing, every link resolves to the same page. When the current page is 1, the library often substitutes the base URL, so test the boundary.__toString() emits raw HTML. In Blade you must use {!! $paginator !!}. Better: loop getPages() and escape $page->url yourself, as shown above.fullUrlWithQuery(['page' => '(:num)']); otherwise filters vanish after page 1.getPages() includes … items whose num is '...' and whose url is null. Guard against linking them ($page->isDisabled is not always true for these — check $page->num or $page->url === null).setTemplate() uses {{placeholder}} substitution; rendering that through Blade::render() will either throw or mangle output.aria-current, the getPages() loop pattern is more future-proof than the bundled template.setTemplate(). The default class names are Bootstrap 3–era (pagination, active, disabled).getPages() in your own presenter class rather than subclassing.str_replace URL rewrites. When adapting Laravel's url(1), replace only the page segment; a naive str_replace('1', '(:num)') will corrupt IDs or slugs elsewhere in the URL.How can I help you explore Laravel packages today?