Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
- New #224: Add filtering by nested values support in `IterableDataReader` (@vjik)
- Chg #225: Rename classes: `All` to `AndX`, `Any` to `OrX`. Remove `Group` class (@vjik)
- Chg #226: Refactor filter classes to use readonly properties instead of getters (@vjik)
- New #213: Add `nextPage()` and `previousPage()` methods to `PaginatorInterface` (@samdark)

## 1.0.1 January 25, 2023

Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,28 @@ $paginator = (new KeysetPaginator($dataReader))
When displaying first page ID (or another field name to paginate by) of the item displayed last is used with `withNextPageToken()`
to get next page.

#### Page-by-page navigation

Both `OffsetPaginator` and `KeysetPaginator` provide `nextPage()` and `previousPage()` methods for easy page-by-page data reading:

```php
$dataReader = (new QueryDataReader($query))->withSort(Sort::only(['id']));
$paginator = (new KeysetPaginator($dataReader))->withPageSize(1000);

// Iterate through all pages
for (
$currentPaginator = $paginator;
$currentPaginator !== null;
$currentPaginator = $currentPaginator->nextPage()
) {
foreach ($currentPaginator->read() as $data) {
// Process each item
}
}
```

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.

## Writing data

```php
Expand Down
16 changes: 16 additions & 0 deletions src/Paginator/KeysetPaginator.php
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,22 @@ public function getNextToken(): ?PageToken
: ($this->currentLastValue === null ? null : PageToken::next($this->currentLastValue));
}

public function nextPage(): ?static
{
$nextToken = $this->getNextToken();
return $nextToken === null
? null
: $this->withToken($nextToken);
}

public function previousPage(): ?static
{
$previousToken = $this->getPreviousToken();
return $previousToken === null
? null
: $this->withToken($previousToken);
}

public function isSortable(): bool
{
return true;
Expand Down
16 changes: 16 additions & 0 deletions src/Paginator/OffsetPaginator.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,22 @@ public function getPreviousToken(): ?PageToken
return $this->isOnFirstPage() ? null : PageToken::next((string) ($this->getCurrentPage() - 1));
}

public function nextPage(): ?static
{
$nextToken = $this->getNextToken();
return $nextToken === null
? null
: $this->withToken($nextToken);
}

public function previousPage(): ?static
{
$previousToken = $this->getPreviousToken();
return $previousToken === null
? null
: $this->withToken($previousToken);
}

public function getPageSize(): int
{
return $this->pageSize;
Expand Down
14 changes: 14 additions & 0 deletions src/Paginator/PaginatorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ public function getNextToken(): ?PageToken;
*/
public function getPreviousToken(): ?PageToken;

/**
* Get a data reader for the next page.
*
* @return static|null Data reader for the next page or `null` if on last page.
*/
public function nextPage(): ?static;

/**
* Get a data reader for the next page.
*
* @return static|null Data reader for the next page or `null` if on last page.
*/
public function previousPage(): ?static;

/**
* Get the maximum number of items per page.
*
Expand Down
82 changes: 82 additions & 0 deletions tests/Paginator/KeysetPaginatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1168,4 +1168,86 @@ public function testGetPageToken(): void

$this->assertSame($token, $paginator->getToken());
}

public function testNextPage(): void
{
$dataSet = [['id' => 1], ['id' => 2], ['id' => 3]];
$sort = Sort::only(['id']);
$dataReader = (new IterableDataReader($dataSet))->withSort($sort);
$paginator = (new KeysetPaginator($dataReader))->withPageSize(2);

// Test first page has next page
$nextPageReader = $paginator->nextPage();
$this->assertInstanceOf(KeysetPaginator::class, $nextPageReader);

// Verify the next page returns correct data
$nextPageData = array_values($this->iterableToArray($nextPageReader->read()));
$this->assertSame([['id' => 3]], $nextPageData);

// Test that returns null when there are no more pages
$this->assertNull($nextPageReader->nextPage());
}

public function testNextPageIterativeReading(): void
{
$dataSet = [['id' => 1], ['id' => 2], ['id' => 3]];
$sort = Sort::only(['id']);
$dataReader = (new IterableDataReader($dataSet))->withSort($sort);
$paginator = (new KeysetPaginator($dataReader))->withPageSize(2);

$allData = [];

// Read all pages iteratively
for (
$currentPaginator = $paginator;
$currentPaginator !== null;
$currentPaginator = $currentPaginator->nextPage()
) {
$pageData = array_values($this->iterableToArray($currentPaginator->read()));
$allData = array_merge($allData, $pageData);
}

// Verify we got all the data
$this->assertSame($dataSet, $allData);
}

public function testPreviousPage(): void
{
$dataSet = [['id' => 1], ['id' => 2], ['id' => 3]];
$sort = Sort::only(['id']);
$dataReader = (new IterableDataReader($dataSet))->withSort($sort);
$paginator = (new KeysetPaginator($dataReader))->withPageSize(2)->withToken(PageToken::next('2'));

// Test reader has previous page
$previousPageReader = $paginator->previousPage();
$this->assertInstanceOf(KeysetPaginator::class, $previousPageReader);

// Verify the previous page returns correct data
$previousPageData = array_values($this->iterableToArray($previousPageReader->read()));
$this->assertSame([['id' => 1], ['id' => 2]], $previousPageData);

// Test that returns null when there are no more pages
$this->assertNull($previousPageReader->previousPage());
}

public function testPreviousPageIterativeReading(): void
{
$dataSet = [['id' => 1], ['id' => 2], ['id' => 3]];
$sort = Sort::only(['id']);
$dataReader = (new IterableDataReader($dataSet))->withSort($sort);
$paginator = (new KeysetPaginator($dataReader))->withPageSize(2)->withToken(PageToken::next('2'));

$allData = [];
$currentPaginator = $paginator;

// Read all pages iteratively
while ($currentPaginator !== null) {
$pageData = array_values($this->iterableToArray($currentPaginator->read()));
$allData = array_merge($pageData, $allData);
$currentPaginator = $currentPaginator->previousPage();
}

// Verify we got all the data
$this->assertSame($dataSet, $allData);
}
}
79 changes: 79 additions & 0 deletions tests/Paginator/OffsetPaginatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -671,4 +671,83 @@ public function testReadOneWithLimit1(): void

$this->assertSame(self::ITEM_1, $result);
}

public function testNextPage(): void
{
$dataSet = [['id' => 1], ['id' => 2], ['id' => 3]];
$dataReader = new IterableDataReader($dataSet);
$paginator = (new OffsetPaginator($dataReader))->withPageSize(2);

// Test first page has next page
$nextPageReader = $paginator->nextPage();
$this->assertInstanceOf(OffsetPaginator::class, $nextPageReader);

// Verify the next page returns correct data
$nextPageData = array_values($this->iterableToArray($nextPageReader->read()));
$this->assertSame([['id' => 3]], $nextPageData);

// Test that returns null when there are no more pages
$this->assertNull($nextPageReader->nextPage());
}

public function testNextPageIterativeReading(): void
{
$dataSet = [['id' => 1], ['id' => 2], ['id' => 3]];
$sort = Sort::only(['id']);
$dataReader = (new IterableDataReader($dataSet))->withSort($sort);
$paginator = (new OffsetPaginator($dataReader))->withPageSize(2);

$allData = [];
$currentPaginator = $paginator;

// Read all pages iteratively
while ($currentPaginator !== null) {
$pageData = array_values($this->iterableToArray($currentPaginator->read()));
$allData = array_merge($allData, $pageData);
$currentPaginator = $currentPaginator->nextPage();
}

// Verify we got all the data
$this->assertSame($dataSet, $allData);
}

public function testPreviousPage(): void
{
$dataSet = [['id' => 1], ['id' => 2], ['id' => 3]];
$sort = Sort::only(['id']);
$dataReader = (new IterableDataReader($dataSet))->withSort($sort);
$paginator = (new OffsetPaginator($dataReader))->withPageSize(2)->withCurrentPage(2);

// Test reader has previous page
$previousPageReader = $paginator->previousPage();
$this->assertInstanceOf(OffsetPaginator::class, $previousPageReader);

// Verify the previous page returns correct data
$previousPageData = array_values($this->iterableToArray($previousPageReader->read()));
$this->assertSame([['id' => 1], ['id' => 2]], $previousPageData);

// Test that returns null when there are no more pages
$this->assertNull($previousPageReader->previousPage());
}

public function testPreviousPageIterativeReading(): void
{
$dataSet = [['id' => 1], ['id' => 2], ['id' => 3]];
$sort = Sort::only(['id']);
$dataReader = (new IterableDataReader($dataSet))->withSort($sort);
$paginator = (new OffsetPaginator($dataReader))->withPageSize(2)->withCurrentPage(2);

$allData = [];
$currentPaginator = $paginator;

// Read all pages iteratively
while ($currentPaginator !== null) {
$pageData = array_values($this->iterableToArray($currentPaginator->read()));
$allData = array_merge($pageData, $allData);
$currentPaginator = $currentPaginator->previousPage();
}

// Verify we got all the data
$this->assertSame($dataSet, $allData);
}
}
Loading