Paginator
final class Paginator (View source)
Pagination arithmetic over one list window (RSRMID-2965).
A response answers five questions about its window from its own wire metadata — where it starts, where it ends, how many rows exist in total, how many were requested, and how many arrived. Everything else a caller asks about paging is arithmetic over those five numbers, and it lives here rather than on {\CNIC\AbstractResponse}: it needs no wire payload, no columns and no records, so tying it to a Response only meant that testing an offset grid required hand-authoring an API response to carry four integers.
This is a value object, and deliberately has no reference to a Response.
Every input is a builtin scalar, so any grid — including ones no brand emits
yet — is one constructor call away. Do not add a fromResponse() convenience
constructor or a ResponseInterface parameter: that would re-couple the two
and put us back where hand-authored payloads were the only way to exercise the
arithmetic (guarded by tests/ResponsePaginationSeamTest.php).
Everything is computed in the constructor, and the getters return what was
computed. That is not an optimisation — seven integer operations need none — it
is where the null handling lives. Each "is this grid usable?" check happens
exactly once, in the branch that also produces the value, so no getter has to
repeat a check that a predicate has already made. On {\CNIC\AbstractResponse}
those checks were duplicated in getNextPageNumber() and
getPreviousPageNumber() — unreachable while the matching predicate held
true, but PHPStan level 9 cannot see across a method boundary, so they had to
stay and be annotated "do not simplify these away".
Offsets, not page numbers, are the grid. Every predicate and every page number is answered from the offset a request would actually start at, so a predicate and its corresponding getter cannot drift apart, and an unaligned window (FIRST=50, LIMIT=100 — "page 1", whose next request starts at 150, not 200) is handled rather than rounded away. Page numbers stay a derived view: the request offsets are exact, and the number reported is the page that offset lands on.
Methods
No description
Get Page Number of the current window, or null when it has no usable offset grid (no metadata, or a non-positive limit — a window of no rows has no meaningful page number).
Check if this list query has a next page.
Check if this list query has a previous page.
Get Page Number of the next list query, or null when there is none.
Get Page Number of the previous list query, or null when there is none.
Get the number of pages available for this list query.
Get all paging data in one hash.
Details
__construct(int|null $first, int|null $last, int|null $total, int|null $limit, int $count)
No description
int|null
getCurrentPageNumber()
Get Page Number of the current window, or null when it has no usable offset grid (no metadata, or a non-positive limit — a window of no rows has no meaningful page number).
bool
hasNextPage()
Check if this list query has a next page.
Answered from the offset grid directly — LAST + 1 < TOTAL — rather than
from page numbers, so it agrees with {\CNIC\getNextPageNumber()} even when
the current window is not aligned to a page boundary.
An empty window is the case to keep in mind, and the reason the
non-positive-limit gate exists. CNR answers one by echoing LAST = FIRST
(with COUNT = 0) rather than by omitting LAST or reporting a row offset.
Observed shapes, all QueryDomainList:
FIRST=0, LIMIT=0 -> count=0, first=0, last=0, total=1825820 FIRST=2000000, LIMIT=0 -> count=0, first=2000000, last=2000000, total=1825824 FIRST=20000000, LIMIT=10 -> count=0, first=20000000, last=20000000, total=1825824
The third self-terminates on the arithmetic, because LAST echoes an offset
far past TOTAL. The first two do not: LAST + 1 < TOTAL holds, and without
a gate CNR\Client::requestNextResponsePage() would advance to FIRST = 1
and re-walk the list from near the start. What stops them is the
non-positive LIMIT gate — the older of the two guards here, which
CNR\Client has relied on to terminate since before the offset grid
existed (see tests/CNR/ClientTest.php testRequestNextResponsePageZeroLimit).
The LAST < FIRST gate is defensive only — no observed CNR response
does it, precisely because an empty window echoes LAST = FIRST. It pins
the invariant the client's advance depends on: since LAST >= FIRST
always, FIRST = LAST + 1 strictly increases and the walk is monotonic. A
future wire change (or a substitute parser) that broke that would send
pagination backwards rather than failing, so it is refused here.
The row count is NOT the gate to use, however much "an empty window has no next page" sounds like the same statement: it describes the rows in hand, not whether more exist beyond them, so a server answering an empty window mid-list would terminate a walk that should have continued.
bool
hasPreviousPage()
Check if this list query has a previous page.
Answered from the offset grid directly — FIRST > 0 — for the same reason
as {\CNIC\hasNextPage()}: an unaligned window still has a well-defined
"before it" even though it does not sit on a page boundary. The same
non-positive-limit gate applies, because a window of no rows cannot page
backward either.
int|null
getNextPageNumber()
Get Page Number of the next list query, or null when there is none.
Computed from the offset the next request will actually start at
(LAST + 1) rather than from the current page number plus one. The two
agree on every window this can be asked about — for a full window LAST + 1
is FIRST + LIMIT, so intdiv(LAST + 1, LIMIT) + 1 reduces to
intdiv(FIRST, LIMIT) + 2; a short window only occurs at the tail, where
{\CNIC\hasNextPage()} is already false. The offset form is used anyway
because it is the same grid the predicate answers from, and because it
mirrors {\CNIC\getPreviousPageNumber()}, where the offset form and
"current - 1" genuinely do differ on an unaligned window.
int|null
getPreviousPageNumber()
Get Page Number of the previous list query, or null when there is none.
Computed from the offset the previous request would start at
(max(0, FIRST - LIMIT)), not from the current page number minus one: for
an unaligned window the two disagree, and the offset form is the one that
matches what would actually be requested. For an aligned FIRST both forms
reduce to the same classic value.
int
getNumberOfPages()
Get the number of pages available for this list query.
0 when total or limit is unavailable and the response holds no rows
(nothing to page through); 1 when it holds rows but is not a paginated
list at all (an implicit single page). Otherwise the ceiling of
total/limit.
array
toArray()
Get all paging data in one hash.
The keys and their order are the wire-facing projection this replaced
(ResponseInterface::getPagination() returned exactly this array before
RSRMID-2965), because it is what CNR\Response::getListHash() publishes
under meta.pg — a table renderer's payload, not an internal shape.