monkeyscloud / monkeyslegion-pagination
ORM-agnostic offset + cursor pagination — PHP 8.4 property hooks, RFC 8288 Link headers, JSON:API links, configurable envelopes
Package info
github.com/MonkeysCloud/MonkeysLegion-Pagination
pkg:composer/monkeyscloud/monkeyslegion-pagination
1.0.0
2026-05-27 02:13 UTC
Requires
- php: ^8.4
Requires (Dev)
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2026-05-27 02:15:24 UTC
README
ORM-agnostic pagination for PHP 8.4 — offset, cursor, and simple paginators with RFC 8288 Link headers, JSON:API links, and property hooks.
Why Another Pagination Package?
| Feature | Laravel | Symfony | ML Pagination |
|---|---|---|---|
| Offset pagination | ✅ LengthAwarePaginator |
✅ PaginatorInterface |
✅ OffsetPaginator |
| Cursor pagination | ✅ CursorPaginator |
❌ | ✅ CursorPaginator |
| Simple (no count) | ✅ Paginator |
❌ | ✅ SimplePaginator |
| PHP 8.4 hooks | ❌ | ❌ | ✅ $p->hasMore, $p->lastPage |
| RFC 8288 Link headers | ❌ manual | ❌ | ✅ toLinkHeader() |
| JSON:API links | ❌ | ❌ | ✅ toJsonApiLinks() |
| ORM-agnostic | ❌ Eloquent-bound | ❌ Doctrine-bound | ✅ any iterable |
| Zero dependencies | ❌ | ❌ | ✅ pure PHP 8.4 |
| Configurable envelope | ❌ fixed format | ❌ | ✅ PaginationResult |
Installation
composer require monkeyscloud/monkeyslegion-pagination
Quick Start
Offset Pagination (with total count)
use MonkeysLegion\Pagination\OffsetPaginator; $paginator = new OffsetPaginator( items: $repository->findPage(page: 3, perPage: 25), total: $repository->count(), page: 3, perPage: 25, ); // PHP 8.4 property hooks $paginator->lastPage; // 4 $paginator->hasMorePages; // true $paginator->from; // 51 $paginator->to; // 75 // Metadata $paginator->toMeta(); // ['current_page' => 3, 'last_page' => 4, ...] // RFC 8288 Link header $paginator->toLinkHeader('/api/users'); // <.../api/users?page=1&per_page=25>; rel="first", <...>; rel="next", ... // JSON:API links $paginator->toJsonApiLinks('/api/users'); // ['self' => '...page[number]=3', 'first' => ..., 'prev' => ..., 'next' => ...]
Cursor Pagination (keyset, no total)
use MonkeysLegion\Pagination\CursorPaginator; $paginator = new CursorPaginator( items: $users, perPage: 25, cursor: $request->getQueryParams()['cursor'] ?? null, nextCursor: $users[count($users) - 1]->id ?? null, previousCursor: $firstId, ); $paginator->hasMorePages; // true $paginator->isFirstPage; // false $paginator->total(); // null (unknown by design)
Simple Pagination (no count query)
use MonkeysLegion\Pagination\SimplePaginator; // Fetch perPage + 1 items → auto-detects hasMore $items = $repository->findAll(limit: 26, offset: 50); $paginator = new SimplePaginator( items: $items, // Pass 26 items → strips to 25, hasMore=true page: 3, perPage: 25, ); $paginator->hasMorePages; // true (auto-detected) $paginator->count(); // 25 $paginator->total(); // null
PaginationResult (envelope + transformer)
use MonkeysLegion\Pagination\PaginationResult; $result = new PaginationResult($paginator); // With transformer callback $json = $result->toJson(fn(User $u) => [ 'id' => $u->id, 'email' => $u->email, ]); // Custom wrapper $result->withWrap('items')->toArray(); // { "items": [...], "meta": { ... } } // No wrapper $result->withWrap(null)->toArray(); // [...]
Response Integration
// In a MonkeysLegion controller return new JsonResponse([ ...(new PaginationResult($paginator))->toArray( fn(User $u) => UserResource::make($u)->toArray(), ), ], headers: [ 'Link' => $paginator->toLinkHeader($request->getUri()->getPath()), ]);
Paginators Comparison
OffsetPaginator |
CursorPaginator |
SimplePaginator |
|
|---|---|---|---|
| Needs total count | ✅ Yes | ❌ No | ❌ No |
| Needs cursor | ❌ No | ✅ Yes | ❌ No |
| O(1) seek | ❌ (OFFSET) | ✅ (WHERE id >) | ❌ (OFFSET) |
| Concurrent-safe | ⚠️ Drift | ✅ Stable | ⚠️ Drift |
lastPage |
✅ | ❌ | ❌ |
| JSON:API links | ✅ | ✅ | ❌ |
| Best for | Admin panels | APIs, feeds | Quick lists |
License
MIT © MonkeysCloud Team