From d73721b84a66eccc17e5ce34a15d7294eb98f320 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Tue, 16 Jan 2024 15:45:55 +0300 Subject: [PATCH 01/12] Add `KeysetPaginator::withValueCaster()` --- CHANGELOG.md | 2 ++ src/Paginator/KeysetPaginator.php | 31 ++++++++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 607bfa89..f86557a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - New #150: Extract `withLimit()` from `ReadableDataInterface` into `LimitableDataInterface` (@vjik) - Enh #150: `PaginatorInterface` now extends `ReadableDataInterface` (@vjik) - Chg #151: Rename `isRequired()` method in `PaginatorInterface` to `isPaginationRequired()` (@vjik) +- New #153: Add `KeysetPaginator::withValueCaster()` method that allows set closure for preparing the page value before + use in data reader filters (@vjik) ## 1.0.1 January 25, 2023 diff --git a/src/Paginator/KeysetPaginator.php b/src/Paginator/KeysetPaginator.php index 3f8b072a..00573e33 100644 --- a/src/Paginator/KeysetPaginator.php +++ b/src/Paginator/KeysetPaginator.php @@ -4,6 +4,7 @@ namespace Yiisoft\Data\Paginator; +use Closure; use InvalidArgumentException; use RuntimeException; use Yiisoft\Arrays\ArrayHelper; @@ -44,6 +45,8 @@ * @template TValue as array|object * * @implements PaginatorInterface + * + * @psalm-type ValueCaster = Closure(string):string */ final class KeysetPaginator implements PaginatorInterface { @@ -73,6 +76,11 @@ final class KeysetPaginator implements PaginatorInterface */ private bool $hasNextPage = false; + /** + * @psalm-var ValueCaster|null + */ + private ?Closure $valueCaster = null; + /** * Reader cache against repeated scans. * See more {@see __clone()} and {@see initialize()}. @@ -158,6 +166,18 @@ public function withPageSize(int $pageSize): static return $new; } + /** + * Returns a new instance with defined closure for preparing the page value before use in data reader filters. + * + * @psalm-param ValueCaster|null $closure + */ + public function withValueCaster(?Closure $closure): self + { + $new = clone $this; + $new->valueCaster = $closure; + return $new; + } + /** * Reads items of the page. * @@ -332,12 +352,17 @@ private function getReverseFilter(Sort $sort): Compare } /** - * @psalm-suppress NullableReturnStatement, InvalidNullableReturnType The code calling this method - * must ensure that at least one of the properties `$firstValue` or `$lastValue` is not `null`. + * @psalm-suppress NullableReturnStatement, InvalidNullableReturnType, PossiblyNullArgument The code calling this + * method must ensure that at least one of the properties `$firstValue` or `$lastValue` is not `null`. */ private function getValue(): string { - return $this->isGoingToPreviousPage() ? $this->firstValue : $this->lastValue; + $value = $this->isGoingToPreviousPage() ? $this->firstValue : $this->lastValue; + if ($this->valueCaster === null) { + return $value; + } + + return call_user_func($this->valueCaster, $value); } private function reverseSort(Sort $sort): Sort From 0cd805b967a7e25d61ef95d5dc27cecc5ec276b9 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Tue, 16 Jan 2024 16:26:00 +0300 Subject: [PATCH 02/12] test --- tests/Paginator/KeysetPaginatorTest.php | 31 +++++++++++ tests/Support/MutationDataReader.php | 71 +++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 tests/Support/MutationDataReader.php diff --git a/tests/Paginator/KeysetPaginatorTest.php b/tests/Paginator/KeysetPaginatorTest.php index ddea7a79..9dfecbb4 100644 --- a/tests/Paginator/KeysetPaginatorTest.php +++ b/tests/Paginator/KeysetPaginatorTest.php @@ -21,6 +21,7 @@ use Yiisoft\Data\Reader\ReadableDataInterface; use Yiisoft\Data\Reader\Sort; use Yiisoft\Data\Reader\SortableDataInterface; +use Yiisoft\Data\Tests\Support\MutationDataReader; use Yiisoft\Data\Tests\TestCase; use function array_values; @@ -738,6 +739,7 @@ public function testImmutability(): void $this->assertNotSame($paginator, $paginator->withNextPageToken('1')); $this->assertNotSame($paginator, $paginator->withPreviousPageToken('1')); $this->assertNotSame($paginator, $paginator->withPageSize(1)); + $this->assertNotSame($paginator, $paginator->withValueCaster(null)); } public function testGetPreviousPageExistForCoverage(): void @@ -795,4 +797,33 @@ public function testGetReverseFilterForCoverage(): void $this->invokeMethod($paginator, 'getReverseFilter', [$sort]), ); } + + public function testValueCaster(): void + { + $dataReader = (new MutationDataReader( + new IterableDataReader(self::DEFAULT_DATASET), + static function ($item) { + $item['id']--; + return $item; + } + ))->withSort(Sort::only(['id'])->withOrderString('id')); + $paginator = (new KeysetPaginator($dataReader)) + ->withPageSize(2) + ->withPreviousPageToken('5') + ->withValueCaster(fn($value) => (string)($value + 1)); + + $this->assertSame( + [ + [ + 'id' => 2, + 'name' => 'Agent K', + ], + [ + 'id' => 4, + 'name' => 'Agent J', + ] + ], + array_values($paginator->read()) + ); + } } diff --git a/tests/Support/MutationDataReader.php b/tests/Support/MutationDataReader.php new file mode 100644 index 00000000..5792fa15 --- /dev/null +++ b/tests/Support/MutationDataReader.php @@ -0,0 +1,71 @@ +decorated = $this->decorated->withFilter($filter); + return $new; + } + + public function withFilterHandlers(FilterHandlerInterface ...$filterHandlers): static + { + $new = clone $this; + $new->decorated = $this->decorated->withFilterHandlers(...$filterHandlers); + return $new; + } + + public function withLimit(int $limit): static + { + $new = clone $this; + $new->decorated = $this->decorated->withLimit($limit); + return $new; + } + + public function read(): iterable + { + return array_map($this->mutation, $this->decorated->read()); + } + + public function readOne(): array|object|null + { + return call_user_func($this->mutation, $this->decorated->readOne()); + } + + public function withSort(?Sort $sort): static + { + $new = clone $this; + $new->decorated = $this->decorated->withSort($sort); + return $new; + } + + public function getSort(): ?Sort + { + return $this->decorated->getSort(); + } +} From e75d798df000601bf880a932fe6ac255c71ba6af Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 16 Jan 2024 13:26:53 +0000 Subject: [PATCH 03/12] Apply fixes from StyleCI --- src/Paginator/KeysetPaginator.php | 2 +- tests/Paginator/KeysetPaginatorTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Paginator/KeysetPaginator.php b/src/Paginator/KeysetPaginator.php index 00573e33..cc4ffd80 100644 --- a/src/Paginator/KeysetPaginator.php +++ b/src/Paginator/KeysetPaginator.php @@ -362,7 +362,7 @@ private function getValue(): string return $value; } - return call_user_func($this->valueCaster, $value); + return ($this->valueCaster)($value); } private function reverseSort(Sort $sort): Sort diff --git a/tests/Paginator/KeysetPaginatorTest.php b/tests/Paginator/KeysetPaginatorTest.php index 9dfecbb4..a7fb7539 100644 --- a/tests/Paginator/KeysetPaginatorTest.php +++ b/tests/Paginator/KeysetPaginatorTest.php @@ -821,7 +821,7 @@ static function ($item) { [ 'id' => 4, 'name' => 'Agent J', - ] + ], ], array_values($paginator->read()) ); From eef9e3ab0920d3770a198225a92594a6b07b0421 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Tue, 16 Jan 2024 16:34:29 +0300 Subject: [PATCH 04/12] fix --- src/Paginator/KeysetPaginator.php | 16 ++++++++-------- tests/Paginator/KeysetPaginatorTest.php | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Paginator/KeysetPaginator.php b/src/Paginator/KeysetPaginator.php index 00573e33..e6467584 100644 --- a/src/Paginator/KeysetPaginator.php +++ b/src/Paginator/KeysetPaginator.php @@ -46,7 +46,7 @@ * * @implements PaginatorInterface * - * @psalm-type ValueCaster = Closure(string):string + * @psalm-type ValueCaster = Closure(string,string):string */ final class KeysetPaginator implements PaginatorInterface { @@ -295,7 +295,6 @@ private function initialize(): void private function readData(ReadableDataInterface $dataReader, Sort $sort): array { $data = []; - /** @var string $field */ [$field] = $this->getFieldAndSortingFromSort($sort); foreach ($dataReader->read() as $key => $item) { @@ -337,17 +336,15 @@ private function previousPageExist(ReadableDataInterface $dataReader, Sort $sort private function getFilter(Sort $sort): Compare { - $value = $this->getValue(); - /** @var string $field */ [$field, $sorting] = $this->getFieldAndSortingFromSort($sort); + $value = $this->getValue($field); return $sorting === 'asc' ? new GreaterThan($field, $value) : new LessThan($field, $value); } private function getReverseFilter(Sort $sort): Compare { - $value = $this->getValue(); - /** @var string $field */ [$field, $sorting] = $this->getFieldAndSortingFromSort($sort); + $value = $this->getValue($field); return $sorting === 'asc' ? new LessThanOrEqual($field, $value) : new GreaterThanOrEqual($field, $value); } @@ -355,14 +352,14 @@ private function getReverseFilter(Sort $sort): Compare * @psalm-suppress NullableReturnStatement, InvalidNullableReturnType, PossiblyNullArgument The code calling this * method must ensure that at least one of the properties `$firstValue` or `$lastValue` is not `null`. */ - private function getValue(): string + private function getValue(string $field): string { $value = $this->isGoingToPreviousPage() ? $this->firstValue : $this->lastValue; if ($this->valueCaster === null) { return $value; } - return call_user_func($this->valueCaster, $value); + return call_user_func($this->valueCaster, $value, $field); } private function reverseSort(Sort $sort): Sort @@ -376,6 +373,9 @@ private function reverseSort(Sort $sort): Sort return $sort->withOrder($order); } + /** + * @return string[] + */ private function getFieldAndSortingFromSort(Sort $sort): array { $order = $sort->getOrder(); diff --git a/tests/Paginator/KeysetPaginatorTest.php b/tests/Paginator/KeysetPaginatorTest.php index 9dfecbb4..93fcd34a 100644 --- a/tests/Paginator/KeysetPaginatorTest.php +++ b/tests/Paginator/KeysetPaginatorTest.php @@ -810,7 +810,7 @@ static function ($item) { $paginator = (new KeysetPaginator($dataReader)) ->withPageSize(2) ->withPreviousPageToken('5') - ->withValueCaster(fn($value) => (string)($value + 1)); + ->withValueCaster(fn($value, $field) => $field === 'id' ? (string)($value + 1) : $value); $this->assertSame( [ From d3907524c8a051cbcc181b0abcb2454ac872a25b Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 16 Jan 2024 13:35:34 +0000 Subject: [PATCH 05/12] Apply fixes from StyleCI --- src/Paginator/KeysetPaginator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Paginator/KeysetPaginator.php b/src/Paginator/KeysetPaginator.php index e6467584..7bbb4660 100644 --- a/src/Paginator/KeysetPaginator.php +++ b/src/Paginator/KeysetPaginator.php @@ -359,7 +359,7 @@ private function getValue(string $field): string return $value; } - return call_user_func($this->valueCaster, $value, $field); + return ($this->valueCaster)($value, $field); } private function reverseSort(Sort $sort): Sort From 6c805de0f1788f3ffebe4d6528163fc40d7156b3 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Tue, 16 Jan 2024 17:33:42 +0300 Subject: [PATCH 06/12] improve --- CHANGELOG.md | 5 +- src/Paginator/KeysetFilterContext.php | 16 ++++++ src/Paginator/KeysetPaginator.php | 69 +++++++++++++++++-------- src/Reader/Filter/Compare.php | 19 ++++++- src/Reader/FilterAssert.php | 2 + tests/Paginator/KeysetPaginatorTest.php | 17 ++++-- 6 files changed, 99 insertions(+), 29 deletions(-) create mode 100644 src/Paginator/KeysetFilterContext.php diff --git a/CHANGELOG.md b/CHANGELOG.md index f86557a5..f0c94521 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,9 @@ - New #150: Extract `withLimit()` from `ReadableDataInterface` into `LimitableDataInterface` (@vjik) - Enh #150: `PaginatorInterface` now extends `ReadableDataInterface` (@vjik) - Chg #151: Rename `isRequired()` method in `PaginatorInterface` to `isPaginationRequired()` (@vjik) -- New #153: Add `KeysetPaginator::withValueCaster()` method that allows set closure for preparing the page value before - use in data reader filters (@vjik) +- New #153: Add `KeysetPaginator::withFilterCallback()` method that allows set closure for preparing filter passed to + the data reader (@vjik) +- New #153: Add `Compare::withValue()` method (@vjik) ## 1.0.1 January 25, 2023 diff --git a/src/Paginator/KeysetFilterContext.php b/src/Paginator/KeysetFilterContext.php new file mode 100644 index 00000000..ac0b6177 --- /dev/null +++ b/src/Paginator/KeysetFilterContext.php @@ -0,0 +1,16 @@ + * - * @psalm-type ValueCaster = Closure(string,string):string + * @psalm-type FilterCallback = Closure(GreaterThan|LessThan|GreaterThanOrEqual|LessThanOrEqual,KeysetFilterContext):FilterInterface */ final class KeysetPaginator implements PaginatorInterface { @@ -77,9 +77,9 @@ final class KeysetPaginator implements PaginatorInterface private bool $hasNextPage = false; /** - * @psalm-var ValueCaster|null + * @psalm-var FilterCallback|null */ - private ?Closure $valueCaster = null; + private ?Closure $filterCallback = null; /** * Reader cache against repeated scans. @@ -169,12 +169,12 @@ public function withPageSize(int $pageSize): static /** * Returns a new instance with defined closure for preparing the page value before use in data reader filters. * - * @psalm-param ValueCaster|null $closure + * @psalm-param FilterCallback|null $callback */ - public function withValueCaster(?Closure $closure): self + public function withFilterCallback(?Closure $callback): self { $new = clone $this; - $new->valueCaster = $closure; + $new->filterCallback = $callback; return $new; } @@ -334,32 +334,57 @@ private function previousPageExist(ReadableDataInterface $dataReader, Sort $sort return !empty($dataReader->withFilter($reverseFilter)->readOne()); } - private function getFilter(Sort $sort): Compare + private function getFilter(Sort $sort): FilterInterface { + $value = $this->getValue(); [$field, $sorting] = $this->getFieldAndSortingFromSort($sort); - $value = $this->getValue($field); - return $sorting === 'asc' ? new GreaterThan($field, $value) : new LessThan($field, $value); + + $filter = $sorting === SORT_ASC ? new GreaterThan($field, $value) : new LessThan($field, $value); + if ($this->filterCallback === null) { + return $filter; + } + + return call_user_func( + $this->filterCallback, + $filter, + new KeysetFilterContext( + $field, + $value, + $sorting, + false, + ) + ); } - private function getReverseFilter(Sort $sort): Compare + private function getReverseFilter(Sort $sort): FilterInterface { + $value = $this->getValue(); [$field, $sorting] = $this->getFieldAndSortingFromSort($sort); - $value = $this->getValue($field); - return $sorting === 'asc' ? new LessThanOrEqual($field, $value) : new GreaterThanOrEqual($field, $value); + + $filter = $sorting === SORT_ASC ? new LessThanOrEqual($field, $value) : new GreaterThanOrEqual($field, $value); + if ($this->filterCallback === null) { + return $filter; + } + + return call_user_func( + $this->filterCallback, + $filter, + new KeysetFilterContext( + $field, + $value, + $sorting, + true, + ) + ); } /** * @psalm-suppress NullableReturnStatement, InvalidNullableReturnType, PossiblyNullArgument The code calling this * method must ensure that at least one of the properties `$firstValue` or `$lastValue` is not `null`. */ - private function getValue(string $field): string + private function getValue(): string { - $value = $this->isGoingToPreviousPage() ? $this->firstValue : $this->lastValue; - if ($this->valueCaster === null) { - return $value; - } - - return call_user_func($this->valueCaster, $value, $field); + return $this->isGoingToPreviousPage() ? $this->firstValue : $this->lastValue; } private function reverseSort(Sort $sort): Sort @@ -374,7 +399,7 @@ private function reverseSort(Sort $sort): Sort } /** - * @return string[] + * @psalm-return array{0: string, 1: int} */ private function getFieldAndSortingFromSort(Sort $sort): array { @@ -382,7 +407,7 @@ private function getFieldAndSortingFromSort(Sort $sort): array return [ (string) key($order), - reset($order), + reset($order) === 'asc' ? SORT_ASC : SORT_DESC, ]; } diff --git a/src/Reader/Filter/Compare.php b/src/Reader/Filter/Compare.php index de833225..39cd4d85 100644 --- a/src/Reader/Filter/Compare.php +++ b/src/Reader/Filter/Compare.php @@ -22,12 +22,27 @@ abstract class Compare implements FilterInterface */ public function __construct(private string $field, mixed $value) { - FilterAssert::isScalarOrInstanceOfDateTimeInterface($value); - $this->value = $value; + $this->setValue($value); + } + + /** + * @param bool|DateTimeInterface|float|int|string $value Value to compare to. + */ + final public function withValue(mixed $value): static + { + $new = clone $this; + $new->setValue($value); + return $new; } public function toCriteriaArray(): array { return [static::getOperator(), $this->field, $this->value]; } + + private function setValue(mixed $value): void + { + FilterAssert::isScalarOrInstanceOfDateTimeInterface($value); + $this->value = $value; + } } diff --git a/src/Reader/FilterAssert.php b/src/Reader/FilterAssert.php index 4efdddfb..c2aa611a 100644 --- a/src/Reader/FilterAssert.php +++ b/src/Reader/FilterAssert.php @@ -75,6 +75,8 @@ public static function isScalar(mixed $value): void * @param mixed $value Value to check. * * @throws InvalidArgumentException If value is not correct. + * + * @psalm-assert DateTimeInterface|scalar $value */ public static function isScalarOrInstanceOfDateTimeInterface(mixed $value): void { diff --git a/tests/Paginator/KeysetPaginatorTest.php b/tests/Paginator/KeysetPaginatorTest.php index 3b49948f..518799b2 100644 --- a/tests/Paginator/KeysetPaginatorTest.php +++ b/tests/Paginator/KeysetPaginatorTest.php @@ -8,6 +8,7 @@ use InvalidArgumentException; use RuntimeException; use stdClass; +use Yiisoft\Data\Paginator\KeysetFilterContext; use Yiisoft\Data\Paginator\KeysetPaginator; use Yiisoft\Data\Reader\Filter\GreaterThan; use Yiisoft\Data\Reader\Filter\GreaterThanOrEqual; @@ -739,7 +740,7 @@ public function testImmutability(): void $this->assertNotSame($paginator, $paginator->withNextPageToken('1')); $this->assertNotSame($paginator, $paginator->withPreviousPageToken('1')); $this->assertNotSame($paginator, $paginator->withPageSize(1)); - $this->assertNotSame($paginator, $paginator->withValueCaster(null)); + $this->assertNotSame($paginator, $paginator->withFilterCallback(null)); } public function testGetPreviousPageExistForCoverage(): void @@ -798,7 +799,7 @@ public function testGetReverseFilterForCoverage(): void ); } - public function testValueCaster(): void + public function testFilterCallback(): void { $dataReader = (new MutationDataReader( new IterableDataReader(self::DEFAULT_DATASET), @@ -810,7 +811,17 @@ static function ($item) { $paginator = (new KeysetPaginator($dataReader)) ->withPageSize(2) ->withPreviousPageToken('5') - ->withValueCaster(fn($value, $field) => $field === 'id' ? (string)($value + 1) : $value); + ->withFilterCallback( + static function ( + GreaterThan|LessThan|GreaterThanOrEqual|LessThanOrEqual $filter, + KeysetFilterContext $context + ): FilterInterface { + if ($context->field === 'id') { + $filter = $filter->withValue((string)($context->value + 1)); + } + return $filter; + } + ); $this->assertSame( [ From 01656075791e26eef99295a5ead4cf0a8d0237cb Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 16 Jan 2024 14:34:57 +0000 Subject: [PATCH 07/12] Apply fixes from StyleCI --- src/Paginator/KeysetPaginator.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Paginator/KeysetPaginator.php b/src/Paginator/KeysetPaginator.php index c0a80bf4..5e67a5b1 100644 --- a/src/Paginator/KeysetPaginator.php +++ b/src/Paginator/KeysetPaginator.php @@ -344,8 +344,7 @@ private function getFilter(Sort $sort): FilterInterface return $filter; } - return call_user_func( - $this->filterCallback, + return ($this->filterCallback)( $filter, new KeysetFilterContext( $field, @@ -366,8 +365,7 @@ private function getReverseFilter(Sort $sort): FilterInterface return $filter; } - return call_user_func( - $this->filterCallback, + return ($this->filterCallback)( $filter, new KeysetFilterContext( $field, From 995d12a80f89f9e36f51d595572385ac67ed3c3c Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Tue, 16 Jan 2024 17:43:29 +0300 Subject: [PATCH 08/12] test --- tests/Reader/Filter/CompareTest.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/Reader/Filter/CompareTest.php diff --git a/tests/Reader/Filter/CompareTest.php b/tests/Reader/Filter/CompareTest.php new file mode 100644 index 00000000..ff4b32b4 --- /dev/null +++ b/tests/Reader/Filter/CompareTest.php @@ -0,0 +1,19 @@ +assertNotSame($filter, $filter->withValue(1)); + $this->assertSame(['<', 'field', 2], $filter->withValue(2)->toCriteriaArray()); + } +} From 2707cc6f034f0aa2340f0ee74ea2a523d4ca6b8b Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Tue, 16 Jan 2024 17:53:13 +0300 Subject: [PATCH 09/12] test --- tests/Paginator/KeysetPaginatorTest.php | 77 +++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/tests/Paginator/KeysetPaginatorTest.php b/tests/Paginator/KeysetPaginatorTest.php index 518799b2..10cba639 100644 --- a/tests/Paginator/KeysetPaginatorTest.php +++ b/tests/Paginator/KeysetPaginatorTest.php @@ -837,4 +837,81 @@ static function ( array_values($paginator->read()) ); } + + public function testFilterCallbackExtended(): void + { + $dataReader = (new MutationDataReader( + new IterableDataReader(self::DEFAULT_DATASET), + static function ($item) { + $item['id']--; + return $item; + } + ))->withSort(Sort::only(['id'])->withOrderString('id')); + $paginator = (new KeysetPaginator($dataReader)) + ->withPageSize(2) + ->withPreviousPageToken('2') + ->withFilterCallback( + static function ( + GreaterThan|LessThan|GreaterThanOrEqual|LessThanOrEqual $filter, + KeysetFilterContext $context + ): FilterInterface { + $value = $context->field === 'id' + ? (string)($context->value + 1) + : $context->value; + + if ($context->isReverse) { + $filter = $context->sorting === SORT_ASC + ? new LessThanOrEqual($context->field, $value) + : new GreaterThanOrEqual($context->field, $value); + } else { + $filter = $context->sorting === SORT_ASC + ? new GreaterThan($context->field, $value) + : new LessThan($context->field, $value); + } + + return $filter; + } + ); + + $this->assertSame( + [ + [ + 'id' => 0, + 'name' => 'Codename Boris', + ], + [ + 'id' => 1, + 'name' => 'Codename Doris', + ], + ], + array_values($paginator->read()) + ); + } + + public function testFilterCallbackWithReverse(): void + { + $dataReader = (new IterableDataReader(self::DEFAULT_DATASET)) + ->withSort(Sort::only(['id'])->withOrderString('id')); + $paginator = (new KeysetPaginator($dataReader)) + ->withPreviousPageToken('1') + ->withFilterCallback( + static function ( + GreaterThan|LessThan|GreaterThanOrEqual|LessThanOrEqual $filter, + KeysetFilterContext $context + ): FilterInterface { + if ($context->isReverse) { + return $context->sorting === SORT_ASC + ? new LessThanOrEqual($context->field, $context->value) + : new GreaterThanOrEqual($context->field, $context->value); + } else { + return $context->sorting === SORT_ASC + ? new GreaterThan($context->field, $context->value) + : new LessThan($context->field, $context->value); + } + } + ); + + $this->assertTrue($paginator->isOnFirstPage()); + $this->assertFalse($paginator->isOnLastPage()); + } } From a108eefafad002a0e31f640d0dc25aec81bcb9bc Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Tue, 16 Jan 2024 15:39:12 +0000 Subject: [PATCH 10/12] Apply fixes from StyleCI --- tests/Paginator/KeysetPaginatorTest.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/Paginator/KeysetPaginatorTest.php b/tests/Paginator/KeysetPaginatorTest.php index 10cba639..e0af89ed 100644 --- a/tests/Paginator/KeysetPaginatorTest.php +++ b/tests/Paginator/KeysetPaginatorTest.php @@ -903,11 +903,10 @@ static function ( return $context->sorting === SORT_ASC ? new LessThanOrEqual($context->field, $context->value) : new GreaterThanOrEqual($context->field, $context->value); - } else { - return $context->sorting === SORT_ASC - ? new GreaterThan($context->field, $context->value) - : new LessThan($context->field, $context->value); } + return $context->sorting === SORT_ASC + ? new GreaterThan($context->field, $context->value) + : new LessThan($context->field, $context->value); } ); From e620196449f2fa4ec6f0c22819db364e0360522f Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Wed, 17 Jan 2024 11:03:38 +0300 Subject: [PATCH 11/12] more tests --- tests/Paginator/KeysetPaginatorTest.php | 131 ++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/tests/Paginator/KeysetPaginatorTest.php b/tests/Paginator/KeysetPaginatorTest.php index e0af89ed..3b9647ae 100644 --- a/tests/Paginator/KeysetPaginatorTest.php +++ b/tests/Paginator/KeysetPaginatorTest.php @@ -8,6 +8,7 @@ use InvalidArgumentException; use RuntimeException; use stdClass; +use Yiisoft\Arrays\ArrayHelper; use Yiisoft\Data\Paginator\KeysetFilterContext; use Yiisoft\Data\Paginator\KeysetPaginator; use Yiisoft\Data\Reader\Filter\GreaterThan; @@ -913,4 +914,134 @@ static function ( $this->assertTrue($paginator->isOnFirstPage()); $this->assertFalse($paginator->isOnLastPage()); } + + public static function dataPageTypeWithPreviousPageToken(): array + { + return [ + /** + * Straight order + * ['id' => 10] + * ['id' => 11] + * ['id' => 12] + * ['id' => 13] + */ + [true, false, [], '8'], + [true, false, [], '9'], + [true, false, [], '10'], + [true, false, [10], '11'], + [true, false, [10, 11], '12'], + [false, false, [11, 12], '13'], + [false, true, [12, 13], '14'], + [false, true, [12, 13], '15'], + + /** + * Reverse order + * ['id' => 13] + * ['id' => 12] + * ['id' => 11] + * ['id' => 10] + */ + [false, true, [11, 10], '8', true], + [false, true, [11, 10], '9', true], + [false, false, [12, 11], '10', true], + [true, false, [13, 12], '11', true], + [true, false, [13], '12', true], + [true, false, [], '13', true], + [true, false, [], '14', true], + [true, false, [], '15', true], + ]; + } + + /** + * @dataProvider dataPageTypeWithPreviousPageToken + */ + public function testPageTypeWithPreviousPageToken( + bool $expectedIsOnFirstPage, + bool $expectedIsOnLastPage, + array $expectedIds, + string $token, + bool $isReverseOrder = false + ): void { + $data = [ + ['id' => 10], + ['id' => 11], + ['id' => 12], + ['id' => 13], + ]; + $sort = Sort::only(['id'])->withOrderString($isReverseOrder ? '-id' : 'id'); + $reader = (new IterableDataReader($data))->withSort($sort); + + $paginator = (new KeysetPaginator($reader)) + ->withPageSize(2) + ->withPreviousPageToken($token); + + $this->assertSame($expectedIsOnFirstPage, $paginator->isOnFirstPage()); + $this->assertSame($expectedIsOnLastPage, $paginator->isOnLastPage()); + $this->assertSame($expectedIds, ArrayHelper::getColumn($paginator->read(), 'id', keepKeys: false)); + } + + public static function dataPageTypeWithNextPageToken(): array + { + return [ + /** + * Straight order + * ['id' => 10] + * ['id' => 11] + * ['id' => 12] + * ['id' => 13] + */ + [true, false, [10, 11], '8'], + [true, false, [10, 11], '9'], + [false, false, [11, 12], '10'], + [false, true, [12, 13], '11'], + [false, true, [13], '12'], + [false, true, [], '13'], + [false, true, [], '14'], + [false, true, [], '15'], + + /** + * Reverse order + * ['id' => 13] + * ['id' => 12] + * ['id' => 11] + * ['id' => 10] + */ + [false, true, [], '8', true], + [false, true, [], '9', true], + [false, true, [], '10', true], + [false, true, [10], '11', true], + [false, true, [11, 10], '12', true], + [false, false, [12, 11], '13', true], + [true, false, [13, 12], '14', true], + [true, false, [13, 12], '15', true], + ]; + } + + /** + * @dataProvider dataPageTypeWithNextPageToken + */ + public function testPageTypeWithNextPageToken( + bool $expectedIsOnFirstPage, + bool $expectedIsOnLastPage, + array $expectedIds, + string $token, + bool $isReverseOrder = false + ): void { + $data = [ + ['id' => 10], + ['id' => 11], + ['id' => 12], + ['id' => 13], + ]; + $sort = Sort::only(['id'])->withOrderString($isReverseOrder ? '-id' : 'id'); + $reader = (new IterableDataReader($data))->withSort($sort); + + $paginator = (new KeysetPaginator($reader)) + ->withPageSize(2) + ->withNextPageToken($token); + + $this->assertSame($expectedIsOnFirstPage, $paginator->isOnFirstPage()); + $this->assertSame($expectedIsOnLastPage, $paginator->isOnLastPage()); + $this->assertSame($expectedIds, ArrayHelper::getColumn($paginator->read(), 'id', keepKeys: false)); + } } From f562b1d107a2a90684492c92743a3b0d34818958 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Wed, 17 Jan 2024 16:35:59 +0300 Subject: [PATCH 12/12] fix phpdoc --- src/Paginator/KeysetPaginator.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Paginator/KeysetPaginator.php b/src/Paginator/KeysetPaginator.php index 5e67a5b1..e34c8ecf 100644 --- a/src/Paginator/KeysetPaginator.php +++ b/src/Paginator/KeysetPaginator.php @@ -167,9 +167,16 @@ public function withPageSize(int $pageSize): static } /** - * Returns a new instance with defined closure for preparing the page value before use in data reader filters. + * Returns a new instance with defined closure for preparing data reader filters. * - * @psalm-param FilterCallback|null $callback + * @psalm-param FilterCallback|null $callback Closure with signature: + * + * ```php + * function( + * GreaterThan|LessThan|GreaterThanOrEqual|LessThanOrEqual $filter, + * KeysetFilterContext $context + * ): FilterInterface + * ``` */ public function withFilterCallback(?Closure $callback): self {