Pagination
The library supports both client-driven and server-driven pagination.
Client-driven pagination
$top
Limits the number of entities returned:
GET /odata/Products?$top=10Maps to LIMIT 10.
$skip
Skips a number of entities (offset):
GET /odata/Products?$skip=20Maps to OFFSET 20.
Combined
GET /odata/Products?$top=10&$skip=20&$orderby=id ascReturns entities 21-30, sorted by id.
Note: when $skip is used without $top, the resolver adds LIMIT 9223372036854775807 (PHP_INT_MAX) on every database driver, because SQLite and MySQL do not accept an OFFSET without a LIMIT.
Server-driven pagination
When no explicit $top is given, the server applies a default page size from configuration. This prevents unbounded result sets.
How it works
The client optionally sends a
Preferheader:Prefer: odata.maxpagesize=50The server compares the client preference with its configured limits
The effective page size is resolved in this order:
- the client preference from the
Preferheader, if one was sent; - otherwise the server default (
config('odata.pagination.default'), default: 200); - then clamped to the server max (
config('odata.pagination.max'), default: no ceiling).
A client that asks for 500 while the default is 200 gets 500, unless
maxis lower.- the client preference from the
@odata.nextLink
When there are more results than the page size, the response includes a @odata.nextLink:
{
"@odata.context": "...",
"value": [ ... ],
"@odata.nextLink": "http://localhost/odata/Products?$skip=200"
}The client follows the @odata.nextLink to fetch the next page.
The next link carries only $skip
As the example shows, the generated link contains the entity set and $skip — and nothing else. If the original request carried $filter, $select, $orderby, $search, $expand, or $count, those options are not repeated in the next link, so following it verbatim returns an unfiltered, unsorted, unprojected second page.
Until this is fixed, a client that pages a shaped collection must build the follow-up request itself: keep your own query options and replace $skip with the value from the next link (or track the offset yourself).
// don't: fetch(json["@odata.nextLink"])
const skip = new URL(json["@odata.nextLink"], location.origin).searchParams.get("$skip");
const next = `${baseQuery}&$skip=${skip}`;Tracked in the package ROADMAP.md.
Preference-Applied
When a page size is in effect, the response echoes it so the client can tell what it actually got:
Preference-Applied: odata.maxpagesize=50Configuration
In config/odata.php:
'pagination' => [
'max' => 1000, // Maximum page size (clamps client preference)
'default' => 200, // Default when no client preference is sent
],Set max to null for no upper bound.
$count
Request the total count alongside paginated results:
GET /odata/Products?$top=10&$count=trueResponse:
{
"@odata.context": "...",
"@odata.count": 42,
"value": [ ... ]
}The count reflects the total number of matching entities (after $filter and $search), ignoring $top and $skip. See $count for more details.