Skip to content

Commit 069ce45

Browse files
committed
Add cursor pagination range truncation support
1 parent e771697 commit 069ce45

File tree

5 files changed

+45
-1
lines changed

5 files changed

+45
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ and this project adheres to
3838
(including `describedby`)
3939
- Add `Schema\Link` class for defining rich link objects with metadata
4040
- Add `JsonApiError::id(string $id)` method for setting error IDs
41+
- Add `Page::$rangeTruncated` parameter for cursor pagination range truncation
42+
support
4143
- Add full support for JSON:API profiles:
4244
- Parse profile URIs from `Accept` header
4345
- `Context::profileRequested(string $uri): bool` - check if a profile was

docs/pagination.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,11 @@ class PostsResource extends AbstractResource implements
118118
): Page {
119119
// ...
120120

121-
return new Page($results, $isFirstPage, $isLastPage);
121+
return new Page(
122+
results: $results,
123+
isFirstPage: $isFirstPage,
124+
isLastPage: $isLastPage,
125+
);
122126
}
123127

124128
public function itemCursor($model, object $query, Context $context): string
@@ -128,6 +132,24 @@ class PostsResource extends AbstractResource implements
128132
}
129133
```
130134

135+
### Range Pagination
136+
137+
When both `after` and `before` cursors are provided, implementations can support
138+
range-based pagination. If the results were truncated to fit within the
139+
specified range, set the `rangeTruncated` parameter:
140+
141+
```php
142+
return new Page(
143+
results: $results,
144+
isFirstPage: false,
145+
isLastPage: false,
146+
rangeTruncated: true,
147+
);
148+
```
149+
150+
This will add `{"page": {"rangeTruncated": true}}` to the document's `meta`
151+
object to inform clients that not all items in the range were returned.
152+
131153
## Countability
132154

133155
By default, offset pagination won't include a `last` link because there is no

src/Pagination/CursorPagination.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ public function paginate(object $query, Context $context): array
7070
]);
7171
}
7272

73+
if ($page->rangeTruncated !== null) {
74+
$context->documentMeta['page']['rangeTruncated'] = $page->rangeTruncated;
75+
}
76+
7377
return $page->results;
7478
}
7579

src/Pagination/Page.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public function __construct(
88
public array $results,
99
public ?bool $isFirstPage = null,
1010
public ?bool $isLastPage = null,
11+
public ?bool $rangeTruncated = null,
1112
) {
1213
}
1314
}

tests/specification/CursorPaginationTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,19 @@ public function test_unknown_cursor_returns_invalid_parameter_error(): void
163163
$this->assertStringContainsString('after', $error['source']['parameter'] ?? '');
164164
}
165165
}
166+
167+
public function test_range_truncation_meta_is_included_when_results_truncated(): void
168+
{
169+
$response = $this->api->handle(
170+
$this->buildRequest('GET', '/articles')->withQueryParams([
171+
'page' => ['before' => '5', 'size' => '2'],
172+
]),
173+
);
174+
175+
$data = json_decode($response->getBody(), true);
176+
177+
$this->assertArrayHasKey('meta', $data);
178+
$this->assertArrayHasKey('page', $data['meta']);
179+
$this->assertTrue($data['meta']['page']['rangeTruncated']);
180+
}
166181
}

0 commit comments

Comments
 (0)