Skip to content

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=10

Maps to LIMIT 10.

$skip ​

Skips a number of entities (offset):

GET /odata/Products?$skip=20

Maps to OFFSET 20.

Combined ​

GET /odata/Products?$top=10&$skip=20&$orderby=id asc

Returns 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 ​

  1. The client optionally sends a Prefer header: Prefer: odata.maxpagesize=50

  2. The server compares the client preference with its configured limits

  3. The effective page size is resolved in this order:

    • the client preference from the Prefer header, 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 max is lower.

When there are more results than the page size, the response includes a @odata.nextLink:

json
{
  "@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).

js
// 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=50

Configuration ​

In config/odata.php:

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=true

Response:

json
{
  "@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.