deepdiver/laravel-cursor-paginator
Cursor-based pagination for Laravel query builder and Eloquent models, offering simple paginators that page through large datasets efficiently without offset queries.
Before anything else: confirm you actually need this package. It is archived, has zero dependents, and predates Laravel's native cursor pagination (cursorPaginate(), shipped in 8.x). On any Laravel ≥ 8 project, the framework's built-in implementation is the correct choice — it is maintained, handles compound ordering, encrypted cursors, and CursorPaginator JSON shape out of the box. Only reach for this package if you are pinned to an older Laravel (5.x/6.x/7.x) or are maintaining a legacy codebase that already depends on it.
Minimal steps (legacy install only):
composer require deepdiver/laravel-cursor-paginator — check the constraint resolves against your PHP/Laravel version; an archived package will not have widened its illuminate/* constraints, so expect to fork or use --ignore-platform-reqs at your own risk.vendor/deepdiver/laravel-cursor-paginator/ before writing a line of code. There is effectively no documentation, no CI badge, and no issue activity to fall back on — the source and any tests/ directory are your only spec.composer.json (extra.laravel.providers), any publishable config, and the public paginator class. That public class is your entire API surface.WHERE id < ? ORDER BY id DESC LIMIT n semantics with a stable "next" token instead of OFFSET.A sane first call looks like: build an Eloquent/query-builder instance, hand it to the package's paginator along with the cursor column and direction, then serialize the result. Verify the exact method names against the source — do not trust blog posts, they describe Laravel's native paginator, not this one.
Feed endpoint with an opaque cursor. The whole point of cursor pagination is that the client never sees an offset. Pattern: accept ?cursor=<token>&limit=<n>, return { data: [...], next_cursor: "..." | null }. Keep the token opaque — base64 or encrypt it — so clients treat it as a black box and you stay free to change the underlying column later.
Stable, unique ordering key. Cursor pagination only works if the sort key is unique and indexed. ORDER BY created_at alone is broken the moment two rows share a timestamp — you will silently skip or duplicate rows at page boundaries. Use created_at, id as a compound key, or just id. Always add a composite index matching the sort order ((created_at, id)).
Direction reversal for "previous page". Forward pagination is trivial; backward pagination requires flipping both the ORDER BY and the comparison operator, then reversing the returned rows in memory before responding. Implement this once in a small helper (or a Resource/transformer) rather than duplicating the logic per endpoint.
API Resource wrapping. Don't leak the paginator object to the client. Map it through an JsonResource / ResourceCollection and expose exactly data + next_cursor (and prev_cursor if you implemented reversal). This also isolates you from whatever shape the archived package returns, which matters if you later swap to cursorPaginate().
Chunked background work. A less obvious but high-value pattern: use cursor iteration for exports, reindexing, and backfills instead of chunk(). chunk() with OFFSET degrades linearly on large tables and can skip rows if the underlying data mutates mid-run. Cursor-based iteration is O(1) per page and immune to that class of bug. The package's cursor generator is a reasonable fit here even if you never expose it over HTTP.
Integration with Blade/Inertia. Resolve the cursor from the request query string in a form request, pass the resulting collection + next_cursor to the view, and have the frontend store next_cursor in component state. Never reconstruct the cursor client-side from an ID — you lose the ability to change the encoding.
total(). By design there is no count — that's the performance win. If a UI needs "page 3 of 47" or a progress bar, cursor pagination is the wrong tool; use paginate() or precompute counts.composer update will eventually break it. The archived package's illuminate/support constraint is frozen. Expect conflicts when you bump Laravel. Mitigation: fork it into your org, or copy the ~100 lines you actually use into app/Support/ and delete the dependency.<, >) against NULL silently returns no rows, and your feed truncates. Guarantee NOT NULL, or add explicit null-ordering handling.App\Support\CursorFeed service that owns token encoding, tenant scoping, and direction reversal gives you a one-file migration path to native cursorPaginate() later.Model::query()->orderBy('id')->cursorPaginate($perPage) replaces this package almost verbatim, and cursorPaginate accepts the same encoded cursor via ?cursor=. Write your endpoints against your own response shape now, and the swap becomes a two-line change inside CursorFeed instead of a client-facing breaking change.How can I help you explore Laravel packages today?