diff --git a/.github/workflows/composer-require-checker.yml b/.github/workflows/composer-require-checker.yml index a857bce..a93390b 100644 --- a/.github/workflows/composer-require-checker.yml +++ b/.github/workflows/composer-require-checker.yml @@ -31,4 +31,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.1', '8.2', '8.3'] + ['8.1', '8.2', '8.3', '8.4'] diff --git a/.github/workflows/mssql.yml b/.github/workflows/mssql.yml index 689e4a9..a4e0aff 100644 --- a/.github/workflows/mssql.yml +++ b/.github/workflows/mssql.yml @@ -33,6 +33,7 @@ jobs: - 8.1 - 8.2 - 8.3 + - 8.4 mssql: - server: 2022-latest diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index df1db8d..ced4b61 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -27,7 +27,7 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.3'] + ['8.4'] min-covered-msi: 100 secrets: STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} diff --git a/.github/workflows/mysql.yml b/.github/workflows/mysql.yml index 73ed2fd..01ef6ba 100644 --- a/.github/workflows/mysql.yml +++ b/.github/workflows/mysql.yml @@ -40,6 +40,7 @@ jobs: - 8.1 - 8.2 - 8.3 + - 8.4 mysql: - 5.7 diff --git a/.github/workflows/pgsql.yml b/.github/workflows/pgsql.yml index 076db87..a76ec22 100644 --- a/.github/workflows/pgsql.yml +++ b/.github/workflows/pgsql.yml @@ -40,6 +40,7 @@ jobs: - 8.1 - 8.2 - 8.3 + - 8.4 pgsql: - 9 diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml index 457772a..5d6931d 100644 --- a/.github/workflows/rector.yml +++ b/.github/workflows/rector.yml @@ -21,4 +21,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.3'] + ['8.4'] diff --git a/.github/workflows/sqlite.yml b/.github/workflows/sqlite.yml index b42130e..6195dad 100644 --- a/.github/workflows/sqlite.yml +++ b/.github/workflows/sqlite.yml @@ -36,6 +36,7 @@ jobs: - 8.1 - 8.2 - 8.3 + - 8.4 steps: - name: Checkout. diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index e33eca8..d03874d 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -29,4 +29,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.1', '8.2', '8.3'] + ['8.1', '8.2', '8.3', '8.4'] diff --git a/composer.json b/composer.json index e7cc165..f8ac4cc 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "prefer-stable": true, "minimum-stability": "dev", "require": { - "php": "^8.1", + "php": "8.1 - 8.4", "ext-mbstring": "*", "cycle/database": "^2.11", "cycle/orm": "^2.9", @@ -44,7 +44,7 @@ "rector/rector": "^2.1.5", "roave/infection-static-analysis-plugin": "^1.35", "spatie/phpunit-watcher": "^1.24", - "vimeo/psalm": "^5.26", + "vimeo/psalm": "^5.26.1 || ^6.10.3", "vlucas/phpdotenv": "^5.6" }, "autoload": { diff --git a/src/Reader/EntityReader.php b/src/Reader/EntityReader.php index c80f0c6..8324a53 100644 --- a/src/Reader/EntityReader.php +++ b/src/Reader/EntityReader.php @@ -13,6 +13,7 @@ use Yiisoft\Data\Cycle\Exception\NotSupportedFilterException; use Yiisoft\Data\Cycle\Reader\FilterHandler\LikeHandler\LikeHandlerFactory; use Yiisoft\Data\Reader\DataReaderInterface; +use Yiisoft\Data\Reader\Filter\All; use Yiisoft\Data\Reader\FilterHandlerInterface; use Yiisoft\Data\Reader\FilterInterface; use Yiisoft\Data\Reader\Sort; @@ -34,7 +35,7 @@ final class EntityReader implements DataReaderInterface private ?int $limit = null; private int $offset = 0; private ?Sort $sorting = null; - private ?FilterInterface $filter = null; + private FilterInterface $filter; private CachedCount $countCache; private CachedCollection $itemsCache; private CachedCollection $oneItemCache; @@ -56,7 +57,9 @@ public function __construct(Select|SelectQuery $query) $likeHandler = LikeHandlerFactory::getLikeHandler($this->query->getDriver()?->getType() ?? 'SQLite'); $this->setFilterHandlers( new FilterHandler\AllHandler(), - new FilterHandler\AnyHandler(), + new FilterHandler\NoneHandler(), + new FilterHandler\AndXHandler(), + new FilterHandler\OrXHandler(), new FilterHandler\BetweenHandler(), new FilterHandler\EqualsHandler(), new FilterHandler\EqualsNullHandler(), @@ -68,8 +71,11 @@ public function __construct(Select|SelectQuery $query) $likeHandler, new FilterHandler\NotHandler(), ); + + $this->filter = new All(); } + #[\Override] public function getSort(): ?Sort { return $this->sorting; @@ -78,6 +84,7 @@ public function getSort(): ?Sort /** * @psalm-mutation-free */ + #[\Override] public function withLimit(?int $limit): static { /** @psalm-suppress DocblockTypeContradiction */ @@ -95,6 +102,7 @@ public function withLimit(?int $limit): static /** * @psalm-mutation-free */ + #[\Override] public function withOffset(int $offset): static { $new = clone $this; @@ -108,6 +116,7 @@ public function withOffset(int $offset): static /** * @psalm-mutation-free */ + #[\Override] public function withSort(?Sort $sort): static { $new = clone $this; @@ -122,7 +131,8 @@ public function withSort(?Sort $sort): static /** * @psalm-mutation-free */ - public function withFilter(?FilterInterface $filter): static + #[\Override] + public function withFilter(FilterInterface $filter): static { $new = clone $this; if ($new->filter !== $filter) { @@ -138,6 +148,7 @@ public function withFilter(?FilterInterface $filter): static /** * @psalm-mutation-free */ + #[\Override] public function withAddedFilterHandlers(FilterHandlerInterface ...$filterHandlers): static { $new = clone $this; @@ -150,11 +161,13 @@ public function withAddedFilterHandlers(FilterHandlerInterface ...$filterHandler return $new; } + #[\Override] public function count(): int { return $this->countCache->getCount(); } + #[\Override] public function read(): iterable { if ($this->itemsCache->getCollection() === null) { @@ -164,6 +177,7 @@ public function read(): iterable return $this->itemsCache->getCollection(); } + #[\Override] public function readOne(): null|array|object { if (!$this->oneItemCache->isCollected()) { @@ -181,6 +195,7 @@ public function readOne(): null|array|object /** * Get Iterator without caching */ + #[\Override] public function getIterator(): Generator { yield from $this->itemsCache->getCollection() ?? $this->buildSelectQuery()->getIterator(); @@ -215,9 +230,9 @@ private function buildSelectQuery(): SelectQuery|Select if ($this->limit !== null) { $newQuery->limit($this->limit); } - if ($this->filter !== null) { - $newQuery->andWhere($this->makeFilterClosure($this->filter)); - } + + $newQuery->andWhere($this->makeFilterClosure($this->filter)); + return $newQuery; } @@ -235,9 +250,8 @@ private function makeFilterClosure(FilterInterface $filter): Closure private function resetCountCache(): void { $newQuery = clone $this->query; - if ($this->filter !== null) { - $newQuery->andWhere($this->makeFilterClosure($this->filter)); - } + $newQuery->andWhere($this->makeFilterClosure($this->filter)); + $this->countCache = new CachedCount($newQuery); } @@ -256,16 +270,19 @@ private function normalizeSortingCriteria(array $criteria): array return $criteria; } - public function getFilter(): ?FilterInterface + #[\Override] + public function getFilter(): FilterInterface { return $this->filter; } + #[\Override] public function getLimit(): ?int { return $this->limit; } + #[\Override] public function getOffset(): int { return $this->offset; diff --git a/src/Reader/FilterHandler/AllHandler.php b/src/Reader/FilterHandler/AllHandler.php index 9a3a16a..c6375fe 100644 --- a/src/Reader/FilterHandler/AllHandler.php +++ b/src/Reader/FilterHandler/AllHandler.php @@ -4,8 +4,8 @@ namespace Yiisoft\Data\Cycle\Reader\FilterHandler; +use Cycle\Database\Injection\Expression; use Cycle\ORM\Select\QueryBuilder; -use Yiisoft\Data\Cycle\Exception\NotSupportedFilterException; use Yiisoft\Data\Cycle\Reader\QueryBuilderFilterHandler; use Yiisoft\Data\Reader\Filter\All; use Yiisoft\Data\Reader\FilterHandlerInterface; @@ -13,24 +13,19 @@ final class AllHandler implements QueryBuilderFilterHandler, FilterHandlerInterface { + #[\Override] public function getFilterClass(): string { return All::class; } + #[\Override] public function getAsWhereArguments(FilterInterface $filter, array $handlers): array { /** @var All $filter */ - return [ - static function (QueryBuilder $select) use ($filter, $handlers) { - foreach ($filter->getFilters() as $subFilter) { - $handler = $handlers[$subFilter::class] ?? null; - if ($handler === null) { - throw new NotSupportedFilterException($subFilter::class); - } - $select->andWhere(...$handler->getAsWhereArguments($subFilter, $handlers)); - } + static function (QueryBuilder $select) { + $select->where(new Expression('1 = 1')); }, ]; } diff --git a/src/Reader/FilterHandler/AndXHandler.php b/src/Reader/FilterHandler/AndXHandler.php new file mode 100644 index 0000000..85566b9 --- /dev/null +++ b/src/Reader/FilterHandler/AndXHandler.php @@ -0,0 +1,38 @@ +filters as $subFilter) { + $handler = $handlers[$subFilter::class] ?? null; + if ($handler === null) { + throw new NotSupportedFilterException($subFilter::class); + } + $select->andWhere(...$handler->getAsWhereArguments($subFilter, $handlers)); + } + }, + ]; + } +} diff --git a/src/Reader/FilterHandler/BetweenHandler.php b/src/Reader/FilterHandler/BetweenHandler.php index 4afea6a..3b739e5 100644 --- a/src/Reader/FilterHandler/BetweenHandler.php +++ b/src/Reader/FilterHandler/BetweenHandler.php @@ -11,15 +11,17 @@ final class BetweenHandler implements QueryBuilderFilterHandler, FilterHandlerInterface { + #[\Override] public function getFilterClass(): string { return Between::class; } + #[\Override] public function getAsWhereArguments(FilterInterface $filter, array $handlers): array { /** @var Between $filter */ - return [$filter->getField(), 'between', $filter->getMinValue(), $filter->getMaxValue()]; + return [$filter->field, 'between', $filter->minValue, $filter->maxValue]; } } diff --git a/src/Reader/FilterHandler/EqualsHandler.php b/src/Reader/FilterHandler/EqualsHandler.php index d034a10..933be1a 100644 --- a/src/Reader/FilterHandler/EqualsHandler.php +++ b/src/Reader/FilterHandler/EqualsHandler.php @@ -11,15 +11,17 @@ final class EqualsHandler implements QueryBuilderFilterHandler, FilterHandlerInterface { + #[\Override] public function getFilterClass(): string { return Equals::class; } + #[\Override] public function getAsWhereArguments(FilterInterface $filter, array $handlers): array { /** @var Equals $filter */ - return [$filter->getField(), '=', $filter->getValue()]; + return [$filter->field, '=', $filter->value]; } } diff --git a/src/Reader/FilterHandler/EqualsNullHandler.php b/src/Reader/FilterHandler/EqualsNullHandler.php index 8f97665..7c8f469 100644 --- a/src/Reader/FilterHandler/EqualsNullHandler.php +++ b/src/Reader/FilterHandler/EqualsNullHandler.php @@ -11,15 +11,17 @@ final class EqualsNullHandler implements QueryBuilderFilterHandler, FilterHandlerInterface { + #[\Override] public function getFilterClass(): string { return EqualsNull::class; } + #[\Override] public function getAsWhereArguments(FilterInterface $filter, array $handlers): array { /** @var EqualsNull $filter */ - return [$filter->getField(), '=', null]; + return [$filter->field, '=', null]; } } diff --git a/src/Reader/FilterHandler/GreaterThanHandler.php b/src/Reader/FilterHandler/GreaterThanHandler.php index d7283a3..f152e3f 100644 --- a/src/Reader/FilterHandler/GreaterThanHandler.php +++ b/src/Reader/FilterHandler/GreaterThanHandler.php @@ -11,15 +11,17 @@ final class GreaterThanHandler implements QueryBuilderFilterHandler, FilterHandlerInterface { + #[\Override] public function getFilterClass(): string { return GreaterThan::class; } + #[\Override] public function getAsWhereArguments(FilterInterface $filter, array $handlers): array { /** @var GreaterThan $filter */ - return [$filter->getField(), '>', $filter->getValue()]; + return [$filter->field, '>', $filter->value]; } } diff --git a/src/Reader/FilterHandler/GreaterThanOrEqualHandler.php b/src/Reader/FilterHandler/GreaterThanOrEqualHandler.php index 53b1776..ee3c6ce 100644 --- a/src/Reader/FilterHandler/GreaterThanOrEqualHandler.php +++ b/src/Reader/FilterHandler/GreaterThanOrEqualHandler.php @@ -11,15 +11,17 @@ final class GreaterThanOrEqualHandler implements QueryBuilderFilterHandler, FilterHandlerInterface { + #[\Override] public function getFilterClass(): string { return GreaterThanOrEqual::class; } + #[\Override] public function getAsWhereArguments(FilterInterface $filter, array $handlers): array { /** @var GreaterThanOrEqual $filter */ - return [$filter->getField(), '>=', $filter->getValue()]; + return [$filter->field, '>=', $filter->value]; } } diff --git a/src/Reader/FilterHandler/InHandler.php b/src/Reader/FilterHandler/InHandler.php index 6f44c2d..3ed693d 100644 --- a/src/Reader/FilterHandler/InHandler.php +++ b/src/Reader/FilterHandler/InHandler.php @@ -12,15 +12,17 @@ final class InHandler implements QueryBuilderFilterHandler, FilterHandlerInterface { + #[\Override] public function getFilterClass(): string { return In::class; } + #[\Override] public function getAsWhereArguments(FilterInterface $filter, array $handlers): array { /** @var In $filter */ - return [$filter->getField(), 'in', new Parameter($filter->getValues())]; + return [$filter->field, 'in', new Parameter($filter->values)]; } } diff --git a/src/Reader/FilterHandler/LessThanHandler.php b/src/Reader/FilterHandler/LessThanHandler.php index a35fa86..3ee5997 100644 --- a/src/Reader/FilterHandler/LessThanHandler.php +++ b/src/Reader/FilterHandler/LessThanHandler.php @@ -11,15 +11,17 @@ final class LessThanHandler implements QueryBuilderFilterHandler, FilterHandlerInterface { + #[\Override] public function getFilterClass(): string { return LessThan::class; } + #[\Override] public function getAsWhereArguments(FilterInterface $filter, array $handlers): array { /** @var LessThan $filter */ - return [$filter->getField(), '<', $filter->getValue()]; + return [$filter->field, '<', $filter->value]; } } diff --git a/src/Reader/FilterHandler/LessThanOrEqualHandler.php b/src/Reader/FilterHandler/LessThanOrEqualHandler.php index 22bddf9..a5948ad 100644 --- a/src/Reader/FilterHandler/LessThanOrEqualHandler.php +++ b/src/Reader/FilterHandler/LessThanOrEqualHandler.php @@ -11,15 +11,17 @@ final class LessThanOrEqualHandler implements QueryBuilderFilterHandler, FilterHandlerInterface { + #[\Override] public function getFilterClass(): string { return LessThanOrEqual::class; } + #[\Override] public function getAsWhereArguments(FilterInterface $filter, array $handlers): array { /** @var LessThanOrEqual $filter */ - return [$filter->getField(), '<=', $filter->getValue()]; + return [$filter->field, '<=', $filter->value]; } } diff --git a/src/Reader/FilterHandler/LikeHandler/BaseLikeHandler.php b/src/Reader/FilterHandler/LikeHandler/BaseLikeHandler.php index 1b286b2..ac99db4 100644 --- a/src/Reader/FilterHandler/LikeHandler/BaseLikeHandler.php +++ b/src/Reader/FilterHandler/LikeHandler/BaseLikeHandler.php @@ -4,7 +4,9 @@ namespace Yiisoft\Data\Cycle\Reader\FilterHandler\LikeHandler; +use Stringable; use Yiisoft\Data\Reader\Filter\Like; +use Yiisoft\Data\Reader\Filter\LikeMode; use Yiisoft\Data\Reader\FilterHandlerInterface; abstract class BaseLikeHandler implements FilterHandlerInterface @@ -15,13 +17,19 @@ abstract class BaseLikeHandler implements FilterHandlerInterface '\\' => '\\\\', ]; + #[\Override] public function getFilterClass(): string { return Like::class; } - protected function prepareValue(string $value): string + protected function prepareValue(string|Stringable $value, LikeMode $mode): string { - return '%' . strtr($value, $this->escapingReplacements) . '%'; + $value = strtr((string)$value, $this->escapingReplacements); + return match ($mode) { + LikeMode::Contains => '%' . $value . '%', + LikeMode::StartsWith => $value . '%', + LikeMode::EndsWith => '%' . $value, + }; } } diff --git a/src/Reader/FilterHandler/LikeHandler/MysqlLikeHandler.php b/src/Reader/FilterHandler/LikeHandler/MysqlLikeHandler.php index 2150256..5199c78 100644 --- a/src/Reader/FilterHandler/LikeHandler/MysqlLikeHandler.php +++ b/src/Reader/FilterHandler/LikeHandler/MysqlLikeHandler.php @@ -10,14 +10,15 @@ final class MysqlLikeHandler extends BaseLikeHandler implements QueryBuilderFilterHandler { + #[\Override] public function getAsWhereArguments(FilterInterface $filter, array $handlers): array { /** @var Like $filter */ - if ($filter->getCaseSensitive() !== true) { - return [$filter->getField(), 'like', '%' . $this->prepareValue($filter->getValue()) . '%']; + if ($filter->caseSensitive !== true) { + return [$filter->field, 'like', $this->prepareValue($filter->value, $filter->mode)]; } - return [$filter->getField(), 'like binary', $this->prepareValue($filter->getValue())]; + return [$filter->field, 'like binary', $this->prepareValue($filter->value, $filter->mode)]; } } diff --git a/src/Reader/FilterHandler/LikeHandler/PostgresLikeHandler.php b/src/Reader/FilterHandler/LikeHandler/PostgresLikeHandler.php index 91dd8f1..8c27fef 100644 --- a/src/Reader/FilterHandler/LikeHandler/PostgresLikeHandler.php +++ b/src/Reader/FilterHandler/LikeHandler/PostgresLikeHandler.php @@ -10,14 +10,15 @@ final class PostgresLikeHandler extends BaseLikeHandler implements QueryBuilderFilterHandler { + #[\Override] public function getAsWhereArguments(FilterInterface $filter, array $handlers): array { /** @var Like $filter */ - if ($filter->getCaseSensitive() !== true) { - return [$filter->getField(), 'ilike', $this->prepareValue($filter->getValue())]; + if ($filter->caseSensitive !== true) { + return [$filter->field, 'ilike', $this->prepareValue($filter->value, $filter->mode)]; } - return [$filter->getField(), 'like', $this->prepareValue($filter->getValue())]; + return [$filter->field, 'like', $this->prepareValue($filter->value, $filter->mode)]; } } diff --git a/src/Reader/FilterHandler/LikeHandler/SqlServerLikeHandler.php b/src/Reader/FilterHandler/LikeHandler/SqlServerLikeHandler.php index 43f8b19..3cb5ef3 100644 --- a/src/Reader/FilterHandler/LikeHandler/SqlServerLikeHandler.php +++ b/src/Reader/FilterHandler/LikeHandler/SqlServerLikeHandler.php @@ -16,14 +16,15 @@ public function __construct() unset($this->escapingReplacements['\\']); } + #[\Override] public function getAsWhereArguments(FilterInterface $filter, array $handlers): array { /** @var Like $filter */ - if ($filter->getCaseSensitive() === true) { + if ($filter->caseSensitive === true) { throw new NotSupportedFilterOptionException(optionName: 'caseSensitive', driverType: 'SQLServer'); } - return [$filter->getField(), 'like', $this->prepareValue($filter->getValue())]; + return [$filter->field, 'like', $this->prepareValue($filter->value, $filter->mode)]; } } diff --git a/src/Reader/FilterHandler/LikeHandler/SqliteLikeHandler.php b/src/Reader/FilterHandler/LikeHandler/SqliteLikeHandler.php index 7d51508..5cc9ccb 100644 --- a/src/Reader/FilterHandler/LikeHandler/SqliteLikeHandler.php +++ b/src/Reader/FilterHandler/LikeHandler/SqliteLikeHandler.php @@ -16,14 +16,15 @@ public function __construct() unset($this->escapingReplacements['\\']); } + #[\Override] public function getAsWhereArguments(FilterInterface $filter, array $handlers): array { /** @var Like $filter */ - if ($filter->getCaseSensitive() === true) { + if ($filter->caseSensitive === true) { throw new NotSupportedFilterOptionException(optionName: 'caseSensitive', driverType: 'SQLite'); } - return [$filter->getField(), 'like', $this->prepareValue($filter->getValue())]; + return [$filter->field, 'like', $this->prepareValue($filter->value, $filter->mode)]; } } diff --git a/src/Reader/FilterHandler/NoneHandler.php b/src/Reader/FilterHandler/NoneHandler.php new file mode 100644 index 0000000..d48c64d --- /dev/null +++ b/src/Reader/FilterHandler/NoneHandler.php @@ -0,0 +1,32 @@ +where(new Expression('1 = 0')); + }, + ]; + } +} diff --git a/src/Reader/FilterHandler/NotHandler.php b/src/Reader/FilterHandler/NotHandler.php index bfdac36..75942ec 100644 --- a/src/Reader/FilterHandler/NotHandler.php +++ b/src/Reader/FilterHandler/NotHandler.php @@ -6,8 +6,7 @@ use Yiisoft\Data\Cycle\Exception\NotSupportedFilterException; use Yiisoft\Data\Cycle\Reader\QueryBuilderFilterHandler; -use Yiisoft\Data\Reader\Filter\All; -use Yiisoft\Data\Reader\Filter\Any; +use Yiisoft\Data\Reader\Filter\AndX; use Yiisoft\Data\Reader\Filter\Between; use Yiisoft\Data\Reader\Filter\Equals; use Yiisoft\Data\Reader\Filter\EqualsNull; @@ -18,22 +17,25 @@ use Yiisoft\Data\Reader\Filter\LessThanOrEqual; use Yiisoft\Data\Reader\Filter\Like; use Yiisoft\Data\Reader\Filter\Not; +use Yiisoft\Data\Reader\Filter\OrX; use Yiisoft\Data\Reader\FilterHandlerInterface; use Yiisoft\Data\Reader\FilterInterface; final class NotHandler implements QueryBuilderFilterHandler, FilterHandlerInterface { + #[\Override] public function getFilterClass(): string { return Not::class; } + #[\Override] public function getAsWhereArguments(FilterInterface $filter, array $handlers): array { /** @var Not $filter */ - $convertedFilter = $this->convertFilter($filter->getFilter()); - $handledFilter = $convertedFilter instanceof Not ? $convertedFilter->getFilter() : $convertedFilter; + $convertedFilter = $this->convertFilter($filter->filter); + $handledFilter = $convertedFilter instanceof Not ? $convertedFilter->filter : $convertedFilter; $handler = $handlers[$handledFilter::class] ?? null; if ($handler === null) { throw new NotSupportedFilterException($handledFilter::class); @@ -59,22 +61,22 @@ private function convertFilter(FilterInterface $filter, int $notCount = 1): Filt $handler = $this; return match ($filter::class) { - All::class => new Any( + AndX::class => new OrX( ...array_map( static fn (FilterInterface $subFilter): FilterInterface => $handler->convertFilter($subFilter), - $filter->getFilters(), + $filter->filters, ), ), - Any::class => new All( + OrX::class => new AndX( ...array_map( static fn (FilterInterface $subFilter): FilterInterface => $handler->convertFilter($subFilter), - $filter->getFilters(), + $filter->filters, ), ), - GreaterThan::class => new LessThanOrEqual($filter->getField(), $filter->getValue()), - GreaterThanOrEqual::class => new LessThan($filter->getField(), $filter->getValue()), - LessThan::class => new GreaterThanOrEqual($filter->getField(), $filter->getValue()), - LessThanOrEqual::class => new GreaterThan($filter->getField(), $filter->getValue()), + GreaterThan::class => new LessThanOrEqual($filter->field, $filter->value), + GreaterThanOrEqual::class => new LessThan($filter->field, $filter->value), + LessThan::class => new GreaterThanOrEqual($filter->field, $filter->value), + LessThanOrEqual::class => new GreaterThan($filter->field, $filter->value), Between::class, Equals::class, EqualsNull::class, In::class, Like::class => new Not($filter), Not::class => $this->convertNot($filter, $notCount), default => $filter, @@ -85,10 +87,10 @@ private function convertNot(Not $filter, int $notCount): FilterInterface { $notCount++; - if ($filter->getFilter() instanceof Not) { - return $this->convertFilter($filter->getFilter(), $notCount); + if ($filter->filter instanceof Not) { + return $this->convertFilter($filter->filter, $notCount); } - return $notCount % 2 === 1 ? new Not($filter->getFilter()) : $filter->getFilter(); + return $notCount % 2 === 1 ? new Not($filter->filter) : $filter->filter; } } diff --git a/src/Reader/FilterHandler/AnyHandler.php b/src/Reader/FilterHandler/OrXHandler.php similarity index 79% rename from src/Reader/FilterHandler/AnyHandler.php rename to src/Reader/FilterHandler/OrXHandler.php index 5668951..f6f8925 100644 --- a/src/Reader/FilterHandler/AnyHandler.php +++ b/src/Reader/FilterHandler/OrXHandler.php @@ -7,24 +7,25 @@ use Cycle\ORM\Select\QueryBuilder; use Yiisoft\Data\Cycle\Exception\NotSupportedFilterException; use Yiisoft\Data\Cycle\Reader\QueryBuilderFilterHandler; -use Yiisoft\Data\Reader\Filter\Any; +use Yiisoft\Data\Reader\Filter\OrX; use Yiisoft\Data\Reader\FilterHandlerInterface; use Yiisoft\Data\Reader\FilterInterface; -final class AnyHandler implements QueryBuilderFilterHandler, FilterHandlerInterface +final class OrXHandler implements QueryBuilderFilterHandler, FilterHandlerInterface { + #[\Override] public function getFilterClass(): string { - return Any::class; + return OrX::class; } + #[\Override] public function getAsWhereArguments(FilterInterface $filter, array $handlers): array { - /** @var Any $filter */ - + /** @var OrX $filter */ return [ static function (QueryBuilder $select) use ($filter, $handlers) { - foreach ($filter->getFilters() as $subFilter) { + foreach ($filter->filters as $subFilter) { $handler = $handlers[$subFilter::class] ?? null; if ($handler === null) { throw new NotSupportedFilterException($subFilter::class); diff --git a/src/Writer/EntityWriter.php b/src/Writer/EntityWriter.php index 0891c09..5c0575c 100644 --- a/src/Writer/EntityWriter.php +++ b/src/Writer/EntityWriter.php @@ -17,6 +17,7 @@ public function __construct(private EntityManagerInterface $entityManager) /** * @throws Throwable */ + #[\Override] public function write(iterable $items): void { foreach ($items as $entity) { @@ -25,6 +26,7 @@ public function write(iterable $items): void $this->entityManager->run(); } + #[\Override] public function delete(iterable $items): void { foreach ($items as $entity) { diff --git a/tests/Feature/Base/Reader/BaseEntityReaderTestCase.php b/tests/Feature/Base/Reader/BaseEntityReaderTestCase.php index 45b01a2..0b95fb6 100644 --- a/tests/Feature/Base/Reader/BaseEntityReaderTestCase.php +++ b/tests/Feature/Base/Reader/BaseEntityReaderTestCase.php @@ -36,7 +36,6 @@ public function testReadOneFromItemsCache(): void $reader = (new EntityReader($this->select('user')))->withLimit(3); $ref = (new \ReflectionProperty($reader, 'itemsCache')); - $ref->setAccessible(true); self::assertFalse($ref->getValue($reader)->isCollected()); $reader->read(); @@ -53,7 +52,6 @@ public function testGetIterator(): void $this->assertFixtures([0], [\iterator_to_array($reader->getIterator())[0]]); $ref = (new \ReflectionProperty($reader, 'itemsCache')); - $ref->setAccessible(true); $cache = new CachedCollection(); $cache->setCollection([['foo' => 'bar']]); @@ -96,7 +94,7 @@ public function testCount(): void { $reader = new EntityReader($this->select('user')); - self::assertSame(count(self::$fixtures), $reader->count()); + self::assertSame(count($this->getFixtures()), $reader->count()); } /** @@ -108,7 +106,7 @@ public function testCountWithLimit(): void $this->select('user'), ))->withLimit(1); - self::assertSame(count(self::$fixtures), $reader->count()); + self::assertSame(count($this->getFixtures()), $reader->count()); } public function testCountWithFilter(): void @@ -133,6 +131,12 @@ public function testLimitException(): void (new EntityReader($this->select('user')))->withLimit(-1); } + public function testGetLimit(): void + { + $reader = (new EntityReader($this->select('user')))->withLimit(2); + $this->assertSame(2, $reader->getLimit()); + } + public function testLimitOffset(): void { $reader = (new EntityReader( @@ -142,6 +146,12 @@ public function testLimitOffset(): void $this->assertFixtures([1, 2], $reader->read()); } + public function testGetOffset(): void + { + $reader = (new EntityReader($this->select('user')))->withOffset(1); + $this->assertSame(1, $reader->getOffset()); + } + public function testFilter(): void { $reader = (new EntityReader($this->select('user')))->withFilter(new Equals('number', 2)); @@ -161,6 +171,13 @@ public function testFilterHandlers(): void $reader->read(); } + public function testGetFilter(): void + { + $filter = new Equals('number', 2); + $reader = (new EntityReader($this->select('user')))->withFilter($filter); + $this->assertSame($filter, $reader->getFilter()); + } + public static function dataGetSql(): array { return [ @@ -173,6 +190,7 @@ public static function dataGetSql(): array "user"."balance" AS "c3", "user"."born_at" AS "c4" FROM "user" AS "user" + WHERE ((1 = 1)) LIMIT 2 OFFSET 1 SQL, diff --git a/tests/Feature/Base/Reader/ReaderWithFilter/BaseReaderWithAllTestCase.php b/tests/Feature/Base/Reader/ReaderWithFilter/BaseReaderWithAllTestCase.php index f9a32bf..fdc5227 100644 --- a/tests/Feature/Base/Reader/ReaderWithFilter/BaseReaderWithAllTestCase.php +++ b/tests/Feature/Base/Reader/ReaderWithFilter/BaseReaderWithAllTestCase.php @@ -4,25 +4,9 @@ namespace Yiisoft\Data\Cycle\Tests\Feature\Base\Reader\ReaderWithFilter; -use Yiisoft\Data\Cycle\Exception\NotSupportedFilterException; -use Yiisoft\Data\Cycle\Reader\EntityReader; use Yiisoft\Data\Cycle\Tests\Feature\DataTrait; -use Yiisoft\Data\Cycle\Tests\Support\NotSupportedFilter; -use Yiisoft\Data\Reader\Filter\All; -use Yiisoft\Data\Reader\Filter\Equals; abstract class BaseReaderWithAllTestCase extends \Yiisoft\Data\Tests\Common\Reader\ReaderWithFilter\BaseReaderWithAllTestCase { use DataTrait; - - public function testNotSupportedFilterException(): void - { - $reader = (new EntityReader($this->select('user'))); - - $this->expectException(NotSupportedFilterException::class); - $this->expectExceptionMessage(sprintf('Filter "%s" is not supported.', NotSupportedFilter::class)); - $reader->withFilter( - new All(new Equals('balance', '100.0'), new NotSupportedFilter(), new Equals('email', 'seed@beat')), - ); - } } diff --git a/tests/Feature/Base/Reader/ReaderWithFilter/BaseReaderWithAndXTestCase.php b/tests/Feature/Base/Reader/ReaderWithFilter/BaseReaderWithAndXTestCase.php new file mode 100644 index 0000000..2041ce4 --- /dev/null +++ b/tests/Feature/Base/Reader/ReaderWithFilter/BaseReaderWithAndXTestCase.php @@ -0,0 +1,28 @@ +select('user'))); + + $this->expectException(NotSupportedFilterException::class); + $this->expectExceptionMessage(sprintf('Filter "%s" is not supported.', NotSupportedFilter::class)); + $reader->withFilter( + new AndX(new Equals('balance', '100.0'), new NotSupportedFilter(), new Equals('email', 'seed@beat')), + ); + } +} diff --git a/tests/Feature/Base/Reader/ReaderWithFilter/BaseReaderWithNoneTestCase.php b/tests/Feature/Base/Reader/ReaderWithFilter/BaseReaderWithNoneTestCase.php new file mode 100644 index 0000000..0e4ef60 --- /dev/null +++ b/tests/Feature/Base/Reader/ReaderWithFilter/BaseReaderWithNoneTestCase.php @@ -0,0 +1,12 @@ +expectException(NotSupportedFilterException::class); $this->expectExceptionMessage(sprintf('Filter "%s" is not supported.', NotSupportedFilter::class)); - $reader->withFilter(new Any(new Equals('number', 2), new NotSupportedFilter(), new Equals('number', 3))); + $reader->withFilter(new OrX(new Equals('number', 2), new NotSupportedFilter(), new Equals('number', 3))); } } diff --git a/tests/Feature/DataTrait.php b/tests/Feature/DataTrait.php index 2d86eeb..a318630 100644 --- a/tests/Feature/DataTrait.php +++ b/tests/Feature/DataTrait.php @@ -150,7 +150,7 @@ protected function fillFixtures(): void $user->column('born_at')->date()->nullable(); $user->save(); - $fixtures = static::$fixtures; + $fixtures = $this->getFixtures(); foreach ($fixtures as $index => $fixture) { $fixtures[$index]['balance'] = (string) $fixtures[$index]['balance']; } @@ -205,6 +205,7 @@ private function createSchema(): SchemaInterface 'id' => 'int', 'number' => 'int', 'balance' => 'float', + 'born_at' => 'datetime', ], SchemaInterface::RELATIONS => [], ], @@ -238,7 +239,9 @@ protected function assertFixtures(array $expectedFixtureIndexes, array $actualFi $expectedFixtures = []; foreach ($expectedFixtureIndexes as $index) { - $expectedFixtures[$index] = $this->getFixture($index); + $expectedFixture = $this->getFixture($index); + $expectedFixture['born_at'] = json_decode(json_encode($expectedFixture['born_at']), associative: true); + $expectedFixtures[$index] = $expectedFixture; } $this->assertSame($expectedFixtures, $processedActualFixtures); diff --git a/tests/Feature/Mssql/Reader/EntityReaderTest.php b/tests/Feature/Mssql/Reader/EntityReaderTest.php index ad466aa..4453f0d 100644 --- a/tests/Feature/Mssql/Reader/EntityReaderTest.php +++ b/tests/Feature/Mssql/Reader/EntityReaderTest.php @@ -24,6 +24,7 @@ public static function dataGetSql(): array [user].[born_at] AS [c4], ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS [_ROW_NUMBER_] FROM [user] AS [user] + WHERE ((1=1)) ) AS [ORD_FALLBACK] WHERE [_ROW_NUMBER_] BETWEEN 2 AND 3 SQL, diff --git a/tests/Feature/Mssql/Reader/ReaderWithFilter/ReaderWithAndXTest.php b/tests/Feature/Mssql/Reader/ReaderWithFilter/ReaderWithAndXTest.php new file mode 100644 index 0000000..95a0214 --- /dev/null +++ b/tests/Feature/Mssql/Reader/ReaderWithFilter/ReaderWithAndXTest.php @@ -0,0 +1,12 @@ +expectException(NotSupportedFilterOptionException::class); + $this->expectExceptionMessage('$caseSensitive option is not supported when using SQLServer driver.'); + } + + parent::testWithReader($field, $value, $caseSensitive, $expectedFixtureIndexes); + } } diff --git a/tests/Feature/Mssql/Reader/ReaderWithFilter/ReaderWithNoneTest.php b/tests/Feature/Mssql/Reader/ReaderWithFilter/ReaderWithNoneTest.php new file mode 100644 index 0000000..bf13baf --- /dev/null +++ b/tests/Feature/Mssql/Reader/ReaderWithFilter/ReaderWithNoneTest.php @@ -0,0 +1,12 @@ +expectException(NotSupportedFilterOptionException::class); + $this->expectExceptionMessage('$caseSensitive option is not supported when using SQLite driver.'); + } + + parent::testWithReader($field, $value, $caseSensitive, $expectedFixtureIndexes); + } } diff --git a/tests/Feature/Sqlite/Reader/ReaderWithFilter/ReaderWithNoneTest.php b/tests/Feature/Sqlite/Reader/ReaderWithFilter/ReaderWithNoneTest.php new file mode 100644 index 0000000..f409c03 --- /dev/null +++ b/tests/Feature/Sqlite/Reader/ReaderWithFilter/ReaderWithNoneTest.php @@ -0,0 +1,12 @@ +createMock(SelectQuery::class)); $ref = new \ReflectionMethod($reader, 'normalizeSortingCriteria'); - $ref->setAccessible(true); $this->assertSame( ['number' => 'ASC', 'name' => 'DESC', 'email' => 'ASC'],