Skip to content

Commit

Permalink
Add filter callback to KeysetPaginator and new method `Compare::wit…
Browse files Browse the repository at this point in the history
…hValue()` (#153)
  • Loading branch information
vjik committed Jan 19, 2024
1 parent 41f0d3d commit 562c330
Show file tree
Hide file tree
Showing 8 changed files with 443 additions and 13 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -5,6 +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::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

Expand Down
16 changes: 16 additions & 0 deletions src/Paginator/KeysetFilterContext.php
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Data\Paginator;

final class KeysetFilterContext
{
public function __construct(
public string $field,
public string $value,
public int $sorting,
public bool $isReverse
) {
}
}
77 changes: 66 additions & 11 deletions src/Paginator/KeysetPaginator.php
Expand Up @@ -4,15 +4,16 @@

namespace Yiisoft\Data\Paginator;

use Closure;
use InvalidArgumentException;
use RuntimeException;
use Yiisoft\Arrays\ArrayHelper;
use Yiisoft\Data\Reader\Filter\Compare;
use Yiisoft\Data\Reader\Filter\GreaterThan;
use Yiisoft\Data\Reader\Filter\GreaterThanOrEqual;
use Yiisoft\Data\Reader\Filter\LessThan;
use Yiisoft\Data\Reader\Filter\LessThanOrEqual;
use Yiisoft\Data\Reader\FilterableDataInterface;
use Yiisoft\Data\Reader\FilterInterface;
use Yiisoft\Data\Reader\LimitableDataInterface;
use Yiisoft\Data\Reader\ReadableDataInterface;
use Yiisoft\Data\Reader\Sort;
Expand Down Expand Up @@ -44,6 +45,8 @@
* @template TValue as array|object
*
* @implements PaginatorInterface<TKey, TValue>
*
* @psalm-type FilterCallback = Closure(GreaterThan|LessThan|GreaterThanOrEqual|LessThanOrEqual,KeysetFilterContext):FilterInterface
*/
final class KeysetPaginator implements PaginatorInterface
{
Expand Down Expand Up @@ -73,6 +76,11 @@ final class KeysetPaginator implements PaginatorInterface
*/
private bool $hasNextPage = false;

/**
* @psalm-var FilterCallback|null
*/
private ?Closure $filterCallback = null;

/**
* Reader cache against repeated scans.
* See more {@see __clone()} and {@see initialize()}.
Expand Down Expand Up @@ -158,6 +166,25 @@ public function withPageSize(int $pageSize): static
return $new;
}

/**
* Returns a new instance with defined closure for preparing data reader filters.
*
* @psalm-param FilterCallback|null $callback Closure with signature:
*
* ```php
* function(
* GreaterThan|LessThan|GreaterThanOrEqual|LessThanOrEqual $filter,
* KeysetFilterContext $context
* ): FilterInterface
* ```
*/
public function withFilterCallback(?Closure $callback): self
{
$new = clone $this;
$new->filterCallback = $callback;
return $new;
}

/**
* Reads items of the page.
*
Expand Down Expand Up @@ -275,7 +302,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) {
Expand Down Expand Up @@ -315,25 +341,51 @@ private function previousPageExist(ReadableDataInterface $dataReader, Sort $sort
return !empty($dataReader->withFilter($reverseFilter)->readOne());

Check failure on line 341 in src/Paginator/KeysetPaginator.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.1-ubuntu-latest

RiskyTruthyFalsyComparison

src/Paginator/KeysetPaginator.php:341:17: RiskyTruthyFalsyComparison: Operand of type (TValue:Yiisoft\Data\Paginator\KeysetPaginator as array<array-key, mixed>|object)|null contains type TValue:Yiisoft\Data\Paginator\KeysetPaginator as array<array-key, mixed>|object, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead. (see https://psalm.dev/356)
}

private function getFilter(Sort $sort): Compare
private function getFilter(Sort $sort): FilterInterface
{
$value = $this->getValue();
/** @var string $field */
[$field, $sorting] = $this->getFieldAndSortingFromSort($sort);
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 ($this->filterCallback)(
$filter,
new KeysetFilterContext(
$field,
$value,
$sorting,
false,
)
);
}

private function getReverseFilter(Sort $sort): Compare
private function getReverseFilter(Sort $sort): FilterInterface
{
$value = $this->getValue();
/** @var string $field */
[$field, $sorting] = $this->getFieldAndSortingFromSort($sort);
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 ($this->filterCallback)(
$filter,
new KeysetFilterContext(
$field,
$value,
$sorting,
true,
)
);
}

/**
* @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
{
Expand All @@ -351,13 +403,16 @@ private function reverseSort(Sort $sort): Sort
return $sort->withOrder($order);
}

/**
* @psalm-return array{0: string, 1: int}
*/
private function getFieldAndSortingFromSort(Sort $sort): array
{
$order = $sort->getOrder();

return [
(string) key($order),
reset($order),
reset($order) === 'asc' ? SORT_ASC : SORT_DESC,
];
}

Expand Down
19 changes: 17 additions & 2 deletions src/Reader/Filter/Compare.php
Expand Up @@ -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;
}
}
2 changes: 2 additions & 0 deletions src/Reader/FilterAssert.php
Expand Up @@ -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
{
Expand Down

0 comments on commit 562c330

Please sign in to comment.