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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
- Chg #219: Don't check correctness of current page in `PaginatorInterface::isOnLastPage()` method (@vjik)
- Chg #219: Rename `PaginatorException` to `InvalidPageException` (@vjik)
- Chg #211, #221: Change PHP constraint in `composer.json` to `8.1 - 8.4` (@vjik)
- New #223: Add `Sort::getDefaultOrder()` method (@vjik)
- Enh #223: `KeysetPaginator` now uses default order from `Sort` when no sort is set (@vjik)

## 1.0.1 January 25, 2023

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"require-dev": {
"maglnet/composer-require-checker": "^4.7.1",
"phpunit/phpunit": "^10.5.46",
"rector/rector": "^2.0.15",
"rector/rector": "^2.0.18",
"roave/infection-static-analysis-plugin": "^1.35",
"spatie/phpunit-watcher": "^1.24",
"vimeo/psalm": "^5.26.1 || ^6.10.3"
Expand Down
26 changes: 17 additions & 9 deletions src/Paginator/KeysetPaginator.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Yiisoft\Data\Reader\SortableDataInterface;

use function array_reverse;
use function array_slice;
use function count;
use function key;
use function reset;
Expand Down Expand Up @@ -121,10 +122,7 @@ public function __construct(ReadableDataInterface $dataReader)
throw new InvalidArgumentException('Limited data readers are not supported by keyset pagination.');
}

$sort = $dataReader->getSort();
$this->assertSort($sort);

$this->dataReader = $dataReader;
$this->dataReader = $this->prepareSortInDataReader($dataReader, $dataReader->getSort());
}

public function __clone()
Expand Down Expand Up @@ -255,10 +253,8 @@ public function isSortable(): bool

public function withSort(?Sort $sort): static
{
$this->assertSort($sort);

$new = clone $this;
$new->dataReader = $this->dataReader->withSort($sort);
$new->dataReader = $this->prepareSortInDataReader($this->dataReader, $sort);
return $new;
}

Expand Down Expand Up @@ -436,14 +432,26 @@ private function getFieldAndSortingFromSort(Sort $sort): array
];
}

private function assertSort(?Sort $sort): void
/**
* @param FilterableDataInterface&LimitableDataInterface&ReadableDataInterface<TKey, TValue>&SortableDataInterface $dataReader
* @return FilterableDataInterface&LimitableDataInterface&ReadableDataInterface<TKey, TValue>&SortableDataInterface
*/
private function prepareSortInDataReader(ReadableDataInterface $dataReader, ?Sort $sort): ReadableDataInterface
{
if ($sort === null) {
throw new InvalidArgumentException('Data sorting should be configured to work with keyset pagination.');
}

if (empty($sort->getOrder())) {
throw new InvalidArgumentException('Data should be always sorted to work with keyset pagination.');
$defaultOrder = $sort->getDefaultOrder();
if (empty($defaultOrder)) {
throw new InvalidArgumentException('Data should be always sorted to work with keyset pagination.');
}
$sort = $sort->withOrder(
array_slice($defaultOrder, 0, 1, true)
);
}

return $dataReader->withSort($sort);
}
}
15 changes: 14 additions & 1 deletion src/Reader/Sort.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ final class Sort
private bool $withDefaultSorting = true;

/**
* @var array Logical fields to order by in form of [name => direction].
* @var array Logical fields to order by in form of `[name => direction]`.
* @psalm-var TOrder
*/
private array $currentOrder = [];
Expand Down Expand Up @@ -321,4 +321,17 @@ public function hasFieldInConfig(string $name): bool
{
return isset($this->config[$name]);
}

/**
* Get a default order for logical fields.
*
* @return TOrder
*/
public function getDefaultOrder(): array
{
return array_map(
static fn(array $item) => $item['default'],
$this->config
);
}
}
38 changes: 32 additions & 6 deletions tests/Paginator/KeysetPaginatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -166,15 +166,41 @@ public function testThrowsExceptionForWithSortNull(): void
(new KeysetPaginator($dataReader))->withSort(null);
}

public function testThrowsExceptionWhenNotSorted(): void
public function testDefaultOrderUsage(): void
{
$sort = Sort::only(['id', 'name']);
$sort = Sort::only(['name', 'id']);
$dataReader = (new IterableDataReader(self::getDataSet()))->withSort($sort);
$paginator = new KeysetPaginator($dataReader);

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Data should be always sorted to work with keyset pagination.');
$result = $paginator->read();

new KeysetPaginator($dataReader);
$this->assertSame(
self::getDataSet([4, 3, 2, 0, 1]),
array_values($this->iterableToArray($result)),
);
}

public function testDefaultOrderUsageInPrevious(): void
{
$sort = Sort::only(['name', 'id']);
$data = [
['id' => 2, 'name' => 'A'],
['id' => 1, 'name' => 'A'],
['id' => 3, 'name' => 'B'],
];
$dataReader = (new IterableDataReader($data))->withSort($sort);
$paginator = (new KeysetPaginator($dataReader))
->withToken(PageToken::previous('B'));

$result = $paginator->read();

$this->assertSame(
[
['id' => 2, 'name' => 'A'],
['id' => 1, 'name' => 'A'],
],
array_values($this->iterableToArray($result)),
);
}

public function testThrowsExceptionForWithSortNotSorted(): void
Expand All @@ -185,7 +211,7 @@ public function testThrowsExceptionForWithSortNotSorted(): void
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Data should be always sorted to work with keyset pagination.');

(new KeysetPaginator($dataReader))->withSort(Sort::only(['id', 'name']));
(new KeysetPaginator($dataReader))->withSort(Sort::only([]));
Copy link

Copilot AI Jun 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test expects an exception when using an empty sort, but KeysetPaginator now applies the default order and won’t throw. Consider disabling default sorting on the Sort instance (e.g., via a dedicated API) or updating the test to reflect that an empty sort yields default ordering rather than an error.

Copilot uses AI. Check for mistakes.
}

public function testPageSizeCannotBeLessThanOne(): void
Expand Down
Loading