mindy/pagination
A pagination package for Laravel and PHP applications that helps split large result sets into manageable pages. It provides pagination helpers and rendering utilities, though documentation and community adoption are limited.
This is not a Laravel package. There is no ServiceProvider, no facade, no publishable config, no Blade directive, and no Illuminate\Contracts\Pagination\Paginator implementation. Treat the first hour as a source audit, not an install.
Install and confirm autoloading
composer require mindy/pagination
composer dump-autoload -o
Then immediately check vendor/mindy/pagination/composer.json:
dev-master? Pin to a commit SHA if untagged.psr-4, psr-0, or classmap? PSR-0/classmap mismatches surface as "class not found" at runtime, not install time.Where to look first
src/ — find the paginator class and read the constructor. Typical shape is (total, perPage, currentPage); note whether items are passed in or fetched by the component. Laravel's LengthAwarePaginator is (items, total, perPage, currentPage) — the argument order is different enough to cause silent bugs if you mentally substitute one for the other.views/ or templates/ directory — expect hardcoded Bootstrap 3/4 <ul class="pagination"> markup you'll need to override.tests/ — with 1 star and 0 dependents there is likely nothing. Write boundary tests yourself before shipping.First use case
The lowest-risk use is inside a plain PHP script or an existing MindyPHP app: compute the page window and emit navigation markup from an array or a PDO result set. In a Laravel app, do not introduce it — Laravel's paginate(), simplePaginate(), cursorPaginate(), and links() Blade views already cover this surface and ship with the framework's upgrade guarantees. If you must evaluate it, do so in a scratch branch and do not merge.
Pattern 1 — Manual array slicing (framework-agnostic)
The package's cleanest value is offset arithmetic over an in-memory collection. Slice first, hand the total and window to the paginator, then render.
use Mindy\Pagination\...; // confirm exact FQCN from src/
$page = max(1, (int) ($_GET['page'] ?? 1));
$perPage = 20; // clamp this — see Gotchas
$all = $repository->fetchAll();
$total = count($all);
$items = array_slice($all, ($page - 1) * $perPage, $perPage);
Pattern 2 — PDO / query-builder adapter
The component expects a total and a page, not a live query object. Wrap your data source so the SQL stays yours:
$total = (int) $pdo->query('SELECT COUNT(*) FROM posts')->fetchColumn();
$stmt = $pdo->prepare(
'SELECT * FROM posts ORDER BY id DESC LIMIT :limit OFFSET :offset'
);
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', ($page - 1) * $perPage, PDO::PARAM_INT);
$stmt->execute();
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
Bind LIMIT/OFFSET as integers — string binding on MySQL produces a parse error or silently truncated results. Always include a deterministic ORDER BY or offset pages will shuffle rows between requests.
Pattern 3 — Bridging into Blade (if you insist)
Do not let the package render markup inside a Blade view. Instead, extract just the page-window integers from it and render your own component:
// Controller: compute the window with the package, expose primitives
$paginator = new ...($total, $perPage, $page);
return view('posts.index', [
'items' => $items,
'page' => $paginator->getCurrentPage(),
'lastPage' => $paginator->getTotalPages(), // verify method name in src/
'total' => $total,
]);
{{-- resources/views/components/pager.blade.php --}}
@if ($lastPage > 1)
<nav aria-label="Pagination" class="flex gap-2">
@if ($page > 1)
<a href="{{ request()->fullUrlWithQuery(['page' => $page - 1]) }}">Prev</a>
@endif
@foreach (range(max(1, $page - 2), min($lastPage, $page + 2)) as $p)
<a href="{{ request()->fullUrlWithQuery(['page' => $p]) }}"
@if ($p === $page) aria-current="page" @endif>{{ $p }}</a>
@endforeach
@if ($page < $lastPage)
<a href="{{ request()->fullUrlWithQuery(['page' => $page + 1]) }}">Next</a>
@endif
</nav>
@endif
This is ~30 lines and gives you accessibility, tokenized styling, and test coverage — all things the package does not provide.
Pattern 4 — URL state, always
Preserve filters in the query string (request()->fullUrlWithQuery([...]) or a hand-rolled http_build_query merge). Paginated links that drop search terms are the most common pagination bug in the wild because nobody tests page 2 with an active filter.
Pattern 5 — Abstract at the call site
If pagination touches many controllers, depend on a small interface of your own (PaginatesResults) rather than the concrete package class. That way replacing Mindy's arithmetic with Laravel's LengthAwarePaginator — or a hand-rolled helper — is a binding change, not a refactor.
Pattern 6 — If you're genuinely on MindyPHP
Use the component as its author intended: construct it from the request, hand it the total and per-page, call its render method in the template. The package then integrates naturally with the framework's request object and view engine — that is the one context where you get value without paying the adapter tax.
Class and behavior name collisions. The package's paginator will look similar to LengthAwarePaginator but is not substitutable for it. Never type-hint it where Laravel expects the contract. If you import both in one file you will eventually pass the wrong one.
per_page is a resource-exhaustion vector. If you accept a per-page value from the query string, clamp it hard: $perPage = min(100, max(5, (int) request('per_page', 20)));. An unbounded per_page=1000000 against a live query is a cheap DoS. The package almost certainly does not clamp for you.
Offset pagination scales badly. Deep offsets (LIMIT 20 OFFSET 500000) force index scans and the COUNT(*) dominates on large tables. Worse, under concurrent writes you get duplicate and skipped rows between pages. For admin tables it's fine; for public feeds or APIs, use keyset/cursor pagination (Laravel's cursorPaginate()). Decide this at the data layer once, not per feature.
Zero-result and out-of-range pages. Verify behavior when total = 0 and when page > lastPage. The common failure is a division by zero in ceil($total / $perPage) or a page-1 link when the user expected page 1 clamped from page 999. Add tests for: total 0, total 1, page 1, last page, page beyond last, and exactly-on-the-boundary totals.
Version pinning is non-negotiable. With 1 star, 0 dependents, and no visible release cadence, this is likely a subtree split of the Mindy monorepo — the composer artifact can lag or be force-pushed. Pin to a commit SHA in composer.json ("mindy/pagination": "dev-master#abc1234"), commit composer.lock, and record an internal owner plus an annual review date. Assume you own the code.
No CVE feed, no upgrade guide, no issue triage. Your incident runbook must attribute failures internally. When a PHP minor version deprecates something this package uses, you'll find out at deploy time, not from a changelog.
Markup is almost certainly Bootstrap-era. Expect <ul class="pagination"> with no ARIA attributes and no keyboard handling. Don't try to restyle the package's output with CSS overrides — replace the render step with your own Blade component and take only the integer arithmetic from the package.
Escaping and URL building. If the package builds query strings internally, confirm it uses htmlspecialchars/e() on values and http_build_query/rawurlencode on parameters. Hand-concatenated hrefs are where XSS in pagination controls hides.
Testing strategy. There are likely no tests in the repo. Write a thin wrapper around the package and test that — page 1, last page, out-of-range, zero results, per-page clamping, and filter preservation. If the wrapper's tests pass, you can swap the implementation later without touching call sites.
Extension points are limited. If the class is final, if properties are private with no setters, or if render is a single monolithic method, your only realistic extension point is not calling the render method. Budget for copying the ~50–100 lines of window arithmetic you need (MIT permits this with attribution) rather than fighting the API.
Debugging recipe. When the page control is blank or duplicated: (1) dump the constructor arguments, (2) confirm total is an int not a string, (3) confirm perPage > 0, (4) check the current page is clamped into [1, lastPage]. Nearly every bug in this category is one of those four.
Recommendation for a Laravel codebase. Standardize on the first-party paginator: offset (paginate) for bounded admin tables, cursor (cursorPaginate) for unbounded feeds and JSON APIs, links() with Tailwind/Bootstrap views, and pagination state in the query string. Don't let two pagination paradigms coexist in one codebase — that's how you get three different page parameter names and a support ticket for each.
How can I help you explore Laravel packages today?