Skip to content

Commit 35e32b2

Browse files
authored
Fix #213: Add nextPage() and previousPage() methods to PaginatorInterface
1 parent a4d7079 commit 35e32b2

File tree

7 files changed

+230
-0
lines changed

7 files changed

+230
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
- New #224: Add filtering by nested values support in `IterableDataReader` (@vjik)
5858
- Chg #225: Rename classes: `All` to `AndX`, `Any` to `OrX`. Remove `Group` class (@vjik)
5959
- Chg #226: Refactor filter classes to use readonly properties instead of getters (@vjik)
60+
- New #213: Add `nextPage()` and `previousPage()` methods to `PaginatorInterface` (@samdark)
6061

6162
## 1.0.1 January 25, 2023
6263

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,28 @@ $paginator = (new KeysetPaginator($dataReader))
345345
When displaying first page ID (or another field name to paginate by) of the item displayed last is used with `withNextPageToken()`
346346
to get next page.
347347

348+
#### Page-by-page navigation
349+
350+
Both `OffsetPaginator` and `KeysetPaginator` provide `nextPage()` and `previousPage()` methods for easy page-by-page data reading:
351+
352+
```php
353+
$dataReader = (new QueryDataReader($query))->withSort(Sort::only(['id']));
354+
$paginator = (new KeysetPaginator($dataReader))->withPageSize(1000);
355+
356+
// Iterate through all pages
357+
for (
358+
$currentPaginator = $paginator;
359+
$currentPaginator !== null;
360+
$currentPaginator = $currentPaginator->nextPage()
361+
) {
362+
foreach ($currentPaginator->read() as $data) {
363+
// Process each item
364+
}
365+
}
366+
```
367+
368+
The `nextPage()` method returns a new paginator instance configured for the next page, or `null` when there are no more pages. Similarly, `previousPage()` returns a paginator for the previous page, or `null` when at the first page.
369+
348370
## Writing data
349371

350372
```php

src/Paginator/KeysetPaginator.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,22 @@ public function getNextToken(): ?PageToken
246246
: ($this->currentLastValue === null ? null : PageToken::next($this->currentLastValue));
247247
}
248248

249+
public function nextPage(): ?static
250+
{
251+
$nextToken = $this->getNextToken();
252+
return $nextToken === null
253+
? null
254+
: $this->withToken($nextToken);
255+
}
256+
257+
public function previousPage(): ?static
258+
{
259+
$previousToken = $this->getPreviousToken();
260+
return $previousToken === null
261+
? null
262+
: $this->withToken($previousToken);
263+
}
264+
249265
public function isSortable(): bool
250266
{
251267
return true;

src/Paginator/OffsetPaginator.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,22 @@ public function getPreviousToken(): ?PageToken
156156
return $this->isOnFirstPage() ? null : PageToken::next((string) ($this->getCurrentPage() - 1));
157157
}
158158

159+
public function nextPage(): ?static
160+
{
161+
$nextToken = $this->getNextToken();
162+
return $nextToken === null
163+
? null
164+
: $this->withToken($nextToken);
165+
}
166+
167+
public function previousPage(): ?static
168+
{
169+
$previousToken = $this->getPreviousToken();
170+
return $previousToken === null
171+
? null
172+
: $this->withToken($previousToken);
173+
}
174+
159175
public function getPageSize(): int
160176
{
161177
return $this->pageSize;

src/Paginator/PaginatorInterface.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,20 @@ public function getNextToken(): ?PageToken;
7575
*/
7676
public function getPreviousToken(): ?PageToken;
7777

78+
/**
79+
* Get a data reader for the next page.
80+
*
81+
* @return static|null Data reader for the next page or `null` if on last page.
82+
*/
83+
public function nextPage(): ?static;
84+
85+
/**
86+
* Get a data reader for the next page.
87+
*
88+
* @return static|null Data reader for the next page or `null` if on last page.
89+
*/
90+
public function previousPage(): ?static;
91+
7892
/**
7993
* Get the maximum number of items per page.
8094
*

tests/Paginator/KeysetPaginatorTest.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,4 +1168,86 @@ public function testGetPageToken(): void
11681168

11691169
$this->assertSame($token, $paginator->getToken());
11701170
}
1171+
1172+
public function testNextPage(): void
1173+
{
1174+
$dataSet = [['id' => 1], ['id' => 2], ['id' => 3]];
1175+
$sort = Sort::only(['id']);
1176+
$dataReader = (new IterableDataReader($dataSet))->withSort($sort);
1177+
$paginator = (new KeysetPaginator($dataReader))->withPageSize(2);
1178+
1179+
// Test first page has next page
1180+
$nextPageReader = $paginator->nextPage();
1181+
$this->assertInstanceOf(KeysetPaginator::class, $nextPageReader);
1182+
1183+
// Verify the next page returns correct data
1184+
$nextPageData = array_values($this->iterableToArray($nextPageReader->read()));
1185+
$this->assertSame([['id' => 3]], $nextPageData);
1186+
1187+
// Test that returns null when there are no more pages
1188+
$this->assertNull($nextPageReader->nextPage());
1189+
}
1190+
1191+
public function testNextPageIterativeReading(): void
1192+
{
1193+
$dataSet = [['id' => 1], ['id' => 2], ['id' => 3]];
1194+
$sort = Sort::only(['id']);
1195+
$dataReader = (new IterableDataReader($dataSet))->withSort($sort);
1196+
$paginator = (new KeysetPaginator($dataReader))->withPageSize(2);
1197+
1198+
$allData = [];
1199+
1200+
// Read all pages iteratively
1201+
for (
1202+
$currentPaginator = $paginator;
1203+
$currentPaginator !== null;
1204+
$currentPaginator = $currentPaginator->nextPage()
1205+
) {
1206+
$pageData = array_values($this->iterableToArray($currentPaginator->read()));
1207+
$allData = array_merge($allData, $pageData);
1208+
}
1209+
1210+
// Verify we got all the data
1211+
$this->assertSame($dataSet, $allData);
1212+
}
1213+
1214+
public function testPreviousPage(): void
1215+
{
1216+
$dataSet = [['id' => 1], ['id' => 2], ['id' => 3]];
1217+
$sort = Sort::only(['id']);
1218+
$dataReader = (new IterableDataReader($dataSet))->withSort($sort);
1219+
$paginator = (new KeysetPaginator($dataReader))->withPageSize(2)->withToken(PageToken::next('2'));
1220+
1221+
// Test reader has previous page
1222+
$previousPageReader = $paginator->previousPage();
1223+
$this->assertInstanceOf(KeysetPaginator::class, $previousPageReader);
1224+
1225+
// Verify the previous page returns correct data
1226+
$previousPageData = array_values($this->iterableToArray($previousPageReader->read()));
1227+
$this->assertSame([['id' => 1], ['id' => 2]], $previousPageData);
1228+
1229+
// Test that returns null when there are no more pages
1230+
$this->assertNull($previousPageReader->previousPage());
1231+
}
1232+
1233+
public function testPreviousPageIterativeReading(): void
1234+
{
1235+
$dataSet = [['id' => 1], ['id' => 2], ['id' => 3]];
1236+
$sort = Sort::only(['id']);
1237+
$dataReader = (new IterableDataReader($dataSet))->withSort($sort);
1238+
$paginator = (new KeysetPaginator($dataReader))->withPageSize(2)->withToken(PageToken::next('2'));
1239+
1240+
$allData = [];
1241+
$currentPaginator = $paginator;
1242+
1243+
// Read all pages iteratively
1244+
while ($currentPaginator !== null) {
1245+
$pageData = array_values($this->iterableToArray($currentPaginator->read()));
1246+
$allData = array_merge($pageData, $allData);
1247+
$currentPaginator = $currentPaginator->previousPage();
1248+
}
1249+
1250+
// Verify we got all the data
1251+
$this->assertSame($dataSet, $allData);
1252+
}
11711253
}

tests/Paginator/OffsetPaginatorTest.php

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,4 +671,83 @@ public function testReadOneWithLimit1(): void
671671

672672
$this->assertSame(self::ITEM_1, $result);
673673
}
674+
675+
public function testNextPage(): void
676+
{
677+
$dataSet = [['id' => 1], ['id' => 2], ['id' => 3]];
678+
$dataReader = new IterableDataReader($dataSet);
679+
$paginator = (new OffsetPaginator($dataReader))->withPageSize(2);
680+
681+
// Test first page has next page
682+
$nextPageReader = $paginator->nextPage();
683+
$this->assertInstanceOf(OffsetPaginator::class, $nextPageReader);
684+
685+
// Verify the next page returns correct data
686+
$nextPageData = array_values($this->iterableToArray($nextPageReader->read()));
687+
$this->assertSame([['id' => 3]], $nextPageData);
688+
689+
// Test that returns null when there are no more pages
690+
$this->assertNull($nextPageReader->nextPage());
691+
}
692+
693+
public function testNextPageIterativeReading(): void
694+
{
695+
$dataSet = [['id' => 1], ['id' => 2], ['id' => 3]];
696+
$sort = Sort::only(['id']);
697+
$dataReader = (new IterableDataReader($dataSet))->withSort($sort);
698+
$paginator = (new OffsetPaginator($dataReader))->withPageSize(2);
699+
700+
$allData = [];
701+
$currentPaginator = $paginator;
702+
703+
// Read all pages iteratively
704+
while ($currentPaginator !== null) {
705+
$pageData = array_values($this->iterableToArray($currentPaginator->read()));
706+
$allData = array_merge($allData, $pageData);
707+
$currentPaginator = $currentPaginator->nextPage();
708+
}
709+
710+
// Verify we got all the data
711+
$this->assertSame($dataSet, $allData);
712+
}
713+
714+
public function testPreviousPage(): void
715+
{
716+
$dataSet = [['id' => 1], ['id' => 2], ['id' => 3]];
717+
$sort = Sort::only(['id']);
718+
$dataReader = (new IterableDataReader($dataSet))->withSort($sort);
719+
$paginator = (new OffsetPaginator($dataReader))->withPageSize(2)->withCurrentPage(2);
720+
721+
// Test reader has previous page
722+
$previousPageReader = $paginator->previousPage();
723+
$this->assertInstanceOf(OffsetPaginator::class, $previousPageReader);
724+
725+
// Verify the previous page returns correct data
726+
$previousPageData = array_values($this->iterableToArray($previousPageReader->read()));
727+
$this->assertSame([['id' => 1], ['id' => 2]], $previousPageData);
728+
729+
// Test that returns null when there are no more pages
730+
$this->assertNull($previousPageReader->previousPage());
731+
}
732+
733+
public function testPreviousPageIterativeReading(): void
734+
{
735+
$dataSet = [['id' => 1], ['id' => 2], ['id' => 3]];
736+
$sort = Sort::only(['id']);
737+
$dataReader = (new IterableDataReader($dataSet))->withSort($sort);
738+
$paginator = (new OffsetPaginator($dataReader))->withPageSize(2)->withCurrentPage(2);
739+
740+
$allData = [];
741+
$currentPaginator = $paginator;
742+
743+
// Read all pages iteratively
744+
while ($currentPaginator !== null) {
745+
$pageData = array_values($this->iterableToArray($currentPaginator->read()));
746+
$allData = array_merge($pageData, $allData);
747+
$currentPaginator = $currentPaginator->previousPage();
748+
}
749+
750+
// Verify we got all the data
751+
$this->assertSame($dataSet, $allData);
752+
}
674753
}

0 commit comments

Comments
 (0)