diff --git a/src/AbstractQueryDataReader.php b/src/AbstractQueryDataReader.php index cc9235e..c508644 100644 --- a/src/AbstractQueryDataReader.php +++ b/src/AbstractQueryDataReader.php @@ -40,6 +40,10 @@ abstract class AbstractQueryDataReader implements QueryDataReaderInterface * @psalm-var array|null */ private ?array $data = null; + + /** + * @psalm-var non-negative-int|null + */ private ?int $batchSize = 100; private ?string $countParam = null; @@ -62,9 +66,7 @@ public function __construct( */ public function getIterator(): Generator { - if (is_array($this->data)) { - yield from $this->data; - } elseif ($this->batchSize === null) { + if ($this->batchSize === null) { yield from $this->read(); } else { $iterator = $this->getPreparedQuery()->each($this->batchSize); @@ -104,7 +106,8 @@ public function count(): int public function getPreparedQuery(): QueryInterface { - $query = $this->applyFilter(clone $this->query); + $query = clone $this->query; + $query = $this->applyFilter($query); $query = $this->applyHaving($query); if ($this->limit) { @@ -125,10 +128,9 @@ public function getPreparedQuery(): QueryInterface protected function applyFilter(QueryInterface $query): QueryInterface { if ($this->filter !== null) { - $condition = $this->criteriaHandler->handle($this->filter->toCriteriaArray()); - if ($condition !== null) { - $query = $query->andWhere($condition); - } + return $this->criteriaHandler + ->getHandlerByOperator($this->filter) + ->applyFilter($this->query, $this->filter, $this->criteriaHandler); } return $query; @@ -137,10 +139,9 @@ protected function applyFilter(QueryInterface $query): QueryInterface protected function applyHaving(QueryInterface $query): QueryInterface { if ($this->having !== null) { - $condition = $this->criteriaHandler->handle($this->having->toCriteriaArray()); - if ($condition !== null) { - $query = $query->andHaving($condition); - } + return $this->criteriaHandler + ->getHandlerByOperator($this->having) + ->applyHaving($this->query, $this->having, $this->criteriaHandler); } return $query; @@ -152,6 +153,10 @@ protected function applyHaving(QueryInterface $query): QueryInterface */ public function withOffset(int $offset): static { + if ($this->offset === $offset) { + return $this; + } + $new = clone $this; $new->data = null; $new->offset = $offset; @@ -169,6 +174,10 @@ public function withLimit(int $limit): static throw new InvalidArgumentException('$limit must not be less than 0.'); } + if ($this->limit === $limit) { + return $this; + } + $new = clone $this; $new->data = null; $new->limit = $limit; @@ -242,6 +251,10 @@ public function withBatchSize(?int $batchSize): static throw new InvalidArgumentException('$batchSize cannot be less than 1.'); } + if ($this->batchSize === $batchSize) { + return $this; + } + $new = clone $this; $new->batchSize = $batchSize; diff --git a/src/CriteriaHandler.php b/src/CriteriaHandler.php index cbaa665..9772bce 100644 --- a/src/CriteriaHandler.php +++ b/src/CriteriaHandler.php @@ -8,44 +8,45 @@ use Yiisoft\Data\Db\FilterHandler\AllHandler; use Yiisoft\Data\Db\FilterHandler\AnyHandler; use Yiisoft\Data\Db\FilterHandler\BetweenHandler; -use Yiisoft\Data\Db\FilterHandler\Context; use Yiisoft\Data\Db\FilterHandler\EqualsEmptyHandler; use Yiisoft\Data\Db\FilterHandler\EqualsHandler; use Yiisoft\Data\Db\FilterHandler\EqualsNullHandler; use Yiisoft\Data\Db\FilterHandler\ExistsHandler; use Yiisoft\Data\Db\FilterHandler\GreaterThanHandler; use Yiisoft\Data\Db\FilterHandler\GreaterThanOrEqualHandler; +use Yiisoft\Data\Db\FilterHandler\ILikeHandler; use Yiisoft\Data\Db\FilterHandler\InHandler; use Yiisoft\Data\Db\FilterHandler\LessThanHandler; use Yiisoft\Data\Db\FilterHandler\LessThanOrEqualHandler; use Yiisoft\Data\Db\FilterHandler\LikeHandler; use Yiisoft\Data\Db\FilterHandler\NotHandler; +use Yiisoft\Data\Db\FilterHandler\OrILikeHandler; +use Yiisoft\Data\Db\FilterHandler\OrLikeHandler; use Yiisoft\Data\Db\FilterHandler\QueryHandlerInterface; -use Yiisoft\Data\Reader\FilterHandlerInterface; +use Yiisoft\Data\Reader\FilterInterface; use Yiisoft\Db\Query\QueryPartsInterface; +use function array_merge; +use function array_unshift; + /** * `CriteriaHandler` processes filter criteria array from {@see FilterInterface::toCriteriaArray()} into condition array * that is used in {@see QueryPartsInterface::andWhere()} and {@see QueryPartsInterface::andHaving()}. */ final class CriteriaHandler { - private Context $context; - /** * @psalm-var array */ private array $handlers; /** - * @param QueryHandlerInterface[]|null $handlers - * @param ValueNormalizerInterface|null $valueNormalizer + * @param QueryHandlerInterface ...$handlers */ public function __construct( - ?array $handlers = null, - ValueNormalizerInterface $valueNormalizer = null, + QueryHandlerInterface ...$handlers ) { - if (empty($handlers)) { + if ($handlers === []) { $handlers = [ new AllHandler(), new AnyHandler(), @@ -55,6 +56,9 @@ public function __construct( new LessThanHandler(), new LessThanOrEqualHandler(), new LikeHandler(), + new ILikeHandler(), + new OrLikeHandler(), + new OrILikeHandler(), new InHandler(), new ExistsHandler(), new NotHandler(), @@ -64,51 +68,37 @@ public function __construct( ]; } - $this->handlers = $this->prepareHandlers($handlers); - $this->context = new Context($this, $valueNormalizer ?? new ValueNormalizer()); + $this->handlers = $this->prepareHandlers(...$handlers); } - public function withFilterHandlers(FilterHandlerInterface ...$handlers): self + public function withFilterHandlers(QueryHandlerInterface $handler, QueryHandlerInterface ...$handlers): self { - foreach ($handlers as $handler) { - if (!$handler instanceof QueryHandlerInterface) { - throw new LogicException( - sprintf( - 'Filter handler must implement "%s".', - QueryHandlerInterface::class, - ) - ); - } - } - /** @var QueryHandlerInterface[] $handlers */ + array_unshift($handlers, $handler); $new = clone $this; $new->handlers = array_merge( $this->handlers, - $this->prepareHandlers($handlers), + $this->prepareHandlers(...$handlers), ); return $new; } - public function handle(array $criteria): ?array + public function hasHandler(string|FilterInterface $operator): bool { - if (!isset($criteria[0])) { - throw new LogicException('Incorrect criteria array.'); + if ($operator instanceof FilterInterface) { + $operator = $operator::getOperator(); } - $operator = $criteria[0]; - if (!is_string($operator)) { - throw new LogicException('Criteria operator must be a string.'); - } - - $operands = array_slice($criteria, 1); - - return $this->getHandlerByOperator($operator)->getCondition($operands, $this->context); + return isset($this->handlers[$operator]); } - private function getHandlerByOperator(string $operator): QueryHandlerInterface + public function getHandlerByOperator(string|FilterInterface $operator): QueryHandlerInterface { - if (!isset($this->handlers[$operator])) { + if ($operator instanceof FilterInterface) { + $operator = $operator::getOperator(); + } + + if (!$this->hasHandler($operator)) { throw new LogicException(sprintf('Operator "%s" is not supported', $operator)); } @@ -116,12 +106,12 @@ private function getHandlerByOperator(string $operator): QueryHandlerInterface } /** - * @param QueryHandlerInterface[] $handlers + * @param QueryHandlerInterface ...$handlers * * @return QueryHandlerInterface[] * @psalm-return array */ - private function prepareHandlers(array $handlers): array + private function prepareHandlers(QueryHandlerInterface ...$handlers): array { $result = []; foreach ($handlers as $handler) { diff --git a/src/Filter/All.php b/src/Filter/All.php new file mode 100644 index 0000000..cb45700 --- /dev/null +++ b/src/Filter/All.php @@ -0,0 +1,15 @@ +params = $params; + } + + public static function getOperator(): string + { + return BetweenFilter::getOperator(); + } + + private static function isEmpty(mixed $value): bool + { + return $value === null || $value === ''; + } + + public function toCriteriaArray(): array + { + $isMinEmpty = self::isEmpty($this->min); + $isMaxEmpty = self::isEmpty($this->max); + + if (!$isMinEmpty && !$isMaxEmpty) { + return [ + self::getOperator(), + $this->column, + $this->min, + $this->max, + ]; + } + + if (!$isMinEmpty) { + return (new GreaterThanOrEqual($this->column, $this->min))->toCriteriaArray(); + } + + if (!$isMaxEmpty) { + return (new LessThanOrEqual($this->column, $this->max))->toCriteriaArray(); + } + + return []; + } +} diff --git a/src/Filter/CompareFilter.php b/src/Filter/CompareFilter.php new file mode 100644 index 0000000..fe0f668 --- /dev/null +++ b/src/Filter/CompareFilter.php @@ -0,0 +1,48 @@ +params = $params; + } + + public function withIgnoreNull(bool $ignoreNull = true): static + { + if ($this->ignoreNull === $ignoreNull) { + return $this; + } + + $new = clone $this; + $new->ignoreNull = $ignoreNull; + + return $new; + } + + public function toCriteriaArray(): array + { + if ($this->value === null) { + return $this->ignoreNull ? [] : (new EqualsNull($this->column))->toCriteriaArray(); + } + + return [static::getOperator(), $this->column , $this->value]; + } +} diff --git a/src/Filter/Equals.php b/src/Filter/Equals.php new file mode 100644 index 0000000..1a5d0f7 --- /dev/null +++ b/src/Filter/Equals.php @@ -0,0 +1,25 @@ +value) || $this->value instanceof QueryInterface) { + return (new In($this->column, $this->value))->toCriteriaArray(); + } + + return parent::toCriteriaArray(); + } +} diff --git a/src/Filter/EqualsEmpty.php b/src/Filter/EqualsEmpty.php new file mode 100644 index 0000000..002c643 --- /dev/null +++ b/src/Filter/EqualsEmpty.php @@ -0,0 +1,72 @@ +values = $this->prepareValues(...$values); + } + + + private function prepareValues(bool|string|int|float ...$values): array + { + $unique = []; + + foreach ($values as $value) { + if (!empty($value)) { + $value = is_bool($value) ? ($value ? 'true' : 'false') : $value; + + throw new InvalidArgumentException( + sprintf('$value must be equal php "empty". "%s" given.', $value) + ); + } + + if (!in_array($value, $unique, true)) { + $unique[] = $value; + } + } + + return $unique; + } + + /** + * @inheritDoc + */ + public static function getOperator(): string + { + return FilterEqualsEmpty::getOperator(); + } + + /** + * @inheritDoc + */ + public function toCriteriaArray(): array + { + $filters = [ + new EqualsNull($this->column), + ]; + + if ($this->values) { + $filters[] = new In($this->column, $this->values); + } + + return (new Any(...$filters))->toCriteriaArray(); + } +} diff --git a/src/Filter/EqualsNull.php b/src/Filter/EqualsNull.php new file mode 100644 index 0000000..683eda1 --- /dev/null +++ b/src/Filter/EqualsNull.php @@ -0,0 +1,26 @@ +column, null]; + } +} diff --git a/src/Filter/GreaterThan.php b/src/Filter/GreaterThan.php new file mode 100644 index 0000000..4bceeda --- /dev/null +++ b/src/Filter/GreaterThan.php @@ -0,0 +1,15 @@ +filters = $filters; + } + + /** + * @return array + */ + public function toCriteriaArray(): array + { + $array = [static::getOperator()]; + + foreach ($this->filters as $filter) { + $arr = $filter instanceof FilterInterface ? $filter->toCriteriaArray() : $filter; + + if ($arr !== []) { + $array[] = $arr; + } + } + + return count($array) > 1 ? $array : []; + } + + public function withCriteriaArray(array $criteriaArray): static + { + return static::fromCriteriaArray($criteriaArray); + } + + public function getParams(): array + { + $params = []; + + foreach ($this->filters as $filter) { + if ($filter instanceof QueryFilterInterface && $array = $filter->getParams()) { + $params[] = $array; + } + } + + if (isset($params[1])) { + return array_merge(...$params); + } + + return $params[0] ?? []; + } + + /** + * @param array $criteriaArray + * @return static + * + * @psalm-suppress MixedPropertyTypeCoercion + */ + public static function fromCriteriaArray(array $criteriaArray): static + { + foreach ($criteriaArray as $key => $criteria) { + if (!is_array($criteria)) { + throw new InvalidArgumentException(sprintf('Invalid filter on "%s" key.', $key)); + } + + $operator = array_shift($criteria); + + if (!is_string($operator) || $operator === '') { + throw new InvalidArgumentException(sprintf('Invalid filter operator on "%s" key.', $key)); + } + } + + $filter = new static(); + $filter->filters = $criteriaArray; + + return $filter; + } +} diff --git a/src/Filter/ILike.php b/src/Filter/ILike.php new file mode 100644 index 0000000..4dde9fa --- /dev/null +++ b/src/Filter/ILike.php @@ -0,0 +1,13 @@ +withStart()->withEnd(); + } + + public function withoutBoth(): static + { + return $this->withoutStart()->withoutEnd(); + } + + public function withStart(): static + { + if ($this->start === true) { + return $this; + } + + $new = clone $this; + $new->start = true; + + return $new; + } + + public function withoutStart(): static + { + if ($this->start === false) { + return $this; + } + + $new = clone $this; + $new->start = false; + + return $new; + } + + public function withEnd(): static + { + if ($this->end === true) { + return $this; + } + + $new = clone $this; + $new->end = true; + + return $new; + } + + public function withoutEnd(): static + { + if ($this->end === false) { + return $this; + } + + $new = clone $this; + $new->end = false; + + return $new; + } + + public function toCriteriaArray(): array + { + if (!is_scalar($this->value) || ($this->start && $this->end)) { + return parent::toCriteriaArray(); + } + + if (!$this->start && !$this->end) { + return [static::getOperator(), $this->column, $this->value, null]; + } + + $value = $this->start ? '%' . $this->value : $this->value . '%'; + + return [static::getOperator(), $this->column, $value, null]; + } +} diff --git a/src/Filter/OrILike.php b/src/Filter/OrILike.php new file mode 100644 index 0000000..a9395b1 --- /dev/null +++ b/src/Filter/OrILike.php @@ -0,0 +1,13 @@ +params = $params; + + return $new; + } + + public function withParam(string $name, mixed $value): static + { + $new = clone $this; + $new->params[$name] = $value; + + return $new; + } + + public function getParams(): array + { + return $this->params; + } +} diff --git a/src/Filter/QueryFilterInterface.php b/src/Filter/QueryFilterInterface.php new file mode 100644 index 0000000..1cfb75d --- /dev/null +++ b/src/Filter/QueryFilterInterface.php @@ -0,0 +1,12 @@ +getOperator()) + ); + } + + $operator = array_shift($criteria); + + if (!is_string($operator)) { + throw new InvalidArgumentException( + sprintf('$operator must be type of "string". "%s" given.', get_debug_type($operator)) + ); + } + + return [strtoupper($operator), $criteria]; + } + + /** + * @param array $criteria + * @param CriteriaHandler $criteriaHandler + * @return array|ExpressionInterface|null + * @throws \Yiisoft\Db\Exception\InvalidArgumentException + */ + public function getCondition(array $criteria, CriteriaHandler $criteriaHandler): array|ExpressionInterface|null + { + if ($criteria === []) { + return null; + } + + [$operator, $criteria] = $this->splitCriteria($criteria); + + return match($operator) { + 'OR' => new OrCondition($criteria), + 'AND' => new AndCondition($criteria), + 'IN', 'NOT IN' => InCondition::fromArrayDefinition($operator, $criteria), + 'NOT' => NotCondition::fromArrayDefinition($operator, $criteria), + 'EXISTS', 'NOT EXISTS' => ExistsCondition::fromArrayDefinition($operator, $criteria), + 'BETWEEN', 'NOT BETWEEN' => BetweenCondition::fromArrayDefinition($operator, $criteria), + 'LIKE', 'ILIKE', 'OR LIKE', 'OR ILIKE', 'NOT LIKE', 'NOT ILIKE' => LikeCondition::fromArrayDefinition($operator, $criteria), + default => SimpleCondition::fromArrayDefinition($operator, $criteria), + }; + } + + /** + * @param QueryInterface $query + * @param FilterInterface $filter + * @param CriteriaHandler $criteriaHandler + * @return QueryInterface + * @throws \Yiisoft\Db\Exception\InvalidArgumentException + */ + public function applyFilter(QueryInterface $query, FilterInterface $filter, CriteriaHandler $criteriaHandler): QueryInterface + { + $criteria = $filter->toCriteriaArray(); + $params = $filter instanceof QueryFilterInterface ? $filter->getParams() : []; + + if ($criteria && $condition = $this->getCondition($criteria, $criteriaHandler)) { + $query->andWhere($condition, $params); + } + + return $query; + } + + /** + * @param QueryInterface $query + * @param FilterInterface $filter + * @param CriteriaHandler $criteriaHandler + * @return QueryInterface + * @throws \Yiisoft\Db\Exception\InvalidArgumentException + * + */ + public function applyHaving(QueryInterface $query, FilterInterface $filter, CriteriaHandler $criteriaHandler): QueryInterface + { + $criteria = $filter->toCriteriaArray(); + $params = $filter instanceof QueryFilterInterface ? $filter->getParams() : []; + + if ($criteria && $condition = $this->getCondition($criteria, $criteriaHandler)) { + $query->andHaving($condition, $params); + } + + return $query; + } +} diff --git a/src/FilterHandler/AllHandler.php b/src/FilterHandler/AllHandler.php index 3f31b01..ec62772 100644 --- a/src/FilterHandler/AllHandler.php +++ b/src/FilterHandler/AllHandler.php @@ -4,7 +4,7 @@ namespace Yiisoft\Data\Db\FilterHandler; -use Yiisoft\Data\Reader\Filter\All; +use Yiisoft\Data\Db\Filter\All; final class AllHandler extends GroupHandler { diff --git a/src/FilterHandler/AnyHandler.php b/src/FilterHandler/AnyHandler.php index e8ac314..57a29aa 100644 --- a/src/FilterHandler/AnyHandler.php +++ b/src/FilterHandler/AnyHandler.php @@ -4,7 +4,7 @@ namespace Yiisoft\Data\Db\FilterHandler; -use Yiisoft\Data\Reader\Filter\Any; +use Yiisoft\Data\Db\Filter\Any; final class AnyHandler extends GroupHandler { diff --git a/src/FilterHandler/BetweenHandler.php b/src/FilterHandler/BetweenHandler.php index 3cb6ab2..f78ab6d 100644 --- a/src/FilterHandler/BetweenHandler.php +++ b/src/FilterHandler/BetweenHandler.php @@ -4,32 +4,12 @@ namespace Yiisoft\Data\Db\FilterHandler; -use DateTimeInterface; -use LogicException; use Yiisoft\Data\Reader\Filter\Between; -final class BetweenHandler implements QueryHandlerInterface +final class BetweenHandler extends AbstractHandler { public function getOperator(): string { return Between::getOperator(); } - - public function getCondition(array $operands, Context $context): ?array - { - if ( - array_keys($operands) !== [0, 1, 2] - || !is_string($operands[0]) - || !(is_scalar($operands[1]) || $operands[1] instanceof DateTimeInterface) - || !(is_scalar($operands[2]) || $operands[2] instanceof DateTimeInterface) - ) { - throw new LogicException('Incorrect criteria for the "between" operator.'); - } - return [ - 'BETWEEN', - $operands[0], - $context->normalizeValueToScalar($operands[1]), - $context->normalizeValueToScalar($operands[2]), - ]; - } } diff --git a/src/FilterHandler/CompareHandler.php b/src/FilterHandler/CompareHandler.php deleted file mode 100644 index 963bd4c..0000000 --- a/src/FilterHandler/CompareHandler.php +++ /dev/null @@ -1,26 +0,0 @@ -getOperator())); - } - return [$this->getOperator(), $operands[0], $context->normalizeValueToScalar($operands[1])]; - } -} diff --git a/src/FilterHandler/Context.php b/src/FilterHandler/Context.php deleted file mode 100644 index 1abb10c..0000000 --- a/src/FilterHandler/Context.php +++ /dev/null @@ -1,32 +0,0 @@ -criteriaHandler->handle($criteria); - } - - public function normalizeValueToScalar(mixed $value): bool|string|int|float - { - return $this->valueNormalizer->toScalar($value); - } - - public function normalizeValueToScalarOrNull(mixed $value): bool|string|null|int|float - { - return $this->valueNormalizer->toScalarOrNull($value); - } -} diff --git a/src/FilterHandler/EqualsEmptyHandler.php b/src/FilterHandler/EqualsEmptyHandler.php index 0cdb150..b5e91fe 100644 --- a/src/FilterHandler/EqualsEmptyHandler.php +++ b/src/FilterHandler/EqualsEmptyHandler.php @@ -4,24 +4,26 @@ namespace Yiisoft\Data\Db\FilterHandler; -use LogicException; -use Yiisoft\Data\Reader\Filter\EqualsEmpty; +use Yiisoft\Data\Db\Filter\EqualsEmpty; +use Yiisoft\Data\Reader\Filter\EqualsEmpty as BaseEqualsEmptyFilter; -final class EqualsEmptyHandler implements QueryHandlerInterface +use function strcasecmp; + +final class EqualsEmptyHandler extends AbstractHandler { public function getOperator(): string { return EqualsEmpty::getOperator(); } - public function getCondition(array $operands, Context $context): ?array + protected function splitCriteria(array $criteria): array { - if ( - array_keys($operands) !== [0] - || !is_string($operands[0]) - ) { - throw new LogicException('Incorrect criteria for the "empty" operator.'); + [$operator, $criteria] = parent::splitCriteria($criteria); + + if (strcasecmp($operator, BaseEqualsEmptyFilter::getOperator()) === 0) { + return ['IS', [$criteria[0], null]]; } - return ['OR', ['IS', $operands[0], null], ['=', $operands[0], '']]; + + return [$operator, $criteria]; } } diff --git a/src/FilterHandler/EqualsHandler.php b/src/FilterHandler/EqualsHandler.php index 0bbeebd..62b572c 100644 --- a/src/FilterHandler/EqualsHandler.php +++ b/src/FilterHandler/EqualsHandler.php @@ -4,34 +4,12 @@ namespace Yiisoft\Data\Db\FilterHandler; -use DateTimeInterface; -use LogicException; -use Yiisoft\Data\Reader\Filter\Equals; +use Yiisoft\Data\Db\Filter\Equals; -final class EqualsHandler implements QueryHandlerInterface +final class EqualsHandler extends AbstractHandler { public function getOperator(): string { return Equals::getOperator(); } - - public function getCondition(array $operands, Context $context): ?array - { - if ( - array_keys($operands) !== [0, 1] - || !is_string($operands[0]) - || ( - !is_string($operands[1]) - && !(is_scalar($operands[1]) || null === $operands[1] || $operands[1] instanceof DateTimeInterface) - ) - ) { - throw new LogicException('Incorrect criteria for the "=" operator.'); - } - - if ($operands[1] === null) { - return ['IS NULL', $operands[0]]; - } - - return ['=', $operands[0], $context->normalizeValueToScalarOrNull($operands[1])]; - } } diff --git a/src/FilterHandler/EqualsNullHandler.php b/src/FilterHandler/EqualsNullHandler.php index 96b1fe8..7a57fcb 100644 --- a/src/FilterHandler/EqualsNullHandler.php +++ b/src/FilterHandler/EqualsNullHandler.php @@ -4,24 +4,19 @@ namespace Yiisoft\Data\Db\FilterHandler; -use LogicException; use Yiisoft\Data\Reader\Filter\EqualsNull; -final class EqualsNullHandler implements QueryHandlerInterface +final class EqualsNullHandler extends AbstractHandler { public function getOperator(): string { return EqualsNull::getOperator(); } - public function getCondition(array $operands, Context $context): ?array + protected function splitCriteria(array $criteria): array { - if ( - array_keys($operands) !== [0] - || !is_string($operands[0]) - ) { - throw new LogicException('Incorrect criteria for the "empty" operator.'); - } - return ['IS', $operands[0], null]; + [, $criteria] = parent::splitCriteria($criteria); + + return ['IS', [$criteria[0], null]]; } } diff --git a/src/FilterHandler/ExistsHandler.php b/src/FilterHandler/ExistsHandler.php index aab64ac..23dd94e 100644 --- a/src/FilterHandler/ExistsHandler.php +++ b/src/FilterHandler/ExistsHandler.php @@ -4,26 +4,12 @@ namespace Yiisoft\Data\Db\FilterHandler; -use LogicException; use Yiisoft\Data\Db\Filter\Exists; -use Yiisoft\Db\Query\QueryInterface; -final class ExistsHandler implements QueryHandlerInterface +final class ExistsHandler extends AbstractHandler { public function getOperator(): string { return Exists::getOperator(); } - - public function getCondition(array $operands, Context $context): ?array - { - if ( - array_keys($operands) !== [0] - || !$operands[0] instanceof QueryInterface - ) { - throw new LogicException('Incorrect criteria for the "exists" operator.'); - } - - return ['EXISTS', $operands[0]]; - } } diff --git a/src/FilterHandler/GreaterThanHandler.php b/src/FilterHandler/GreaterThanHandler.php index a2bcf2d..8d7c6d8 100644 --- a/src/FilterHandler/GreaterThanHandler.php +++ b/src/FilterHandler/GreaterThanHandler.php @@ -6,7 +6,7 @@ use Yiisoft\Data\Reader\Filter\GreaterThan; -final class GreaterThanHandler extends CompareHandler +final class GreaterThanHandler extends AbstractHandler { public function getOperator(): string { diff --git a/src/FilterHandler/GreaterThanOrEqualHandler.php b/src/FilterHandler/GreaterThanOrEqualHandler.php index aab9df4..60f7206 100644 --- a/src/FilterHandler/GreaterThanOrEqualHandler.php +++ b/src/FilterHandler/GreaterThanOrEqualHandler.php @@ -6,7 +6,7 @@ use Yiisoft\Data\Reader\Filter\GreaterThanOrEqual; -final class GreaterThanOrEqualHandler extends CompareHandler +final class GreaterThanOrEqualHandler extends AbstractHandler { public function getOperator(): string { diff --git a/src/FilterHandler/GroupHandler.php b/src/FilterHandler/GroupHandler.php index 6b8847e..7674923 100644 --- a/src/FilterHandler/GroupHandler.php +++ b/src/FilterHandler/GroupHandler.php @@ -4,42 +4,77 @@ namespace Yiisoft\Data\Db\FilterHandler; -use LogicException; +use Yiisoft\Data\Db\CriteriaHandler; +use Yiisoft\Db\Expression\ExpressionInterface; +use Yiisoft\Db\QueryBuilder\Condition\AndCondition; +use Yiisoft\Db\QueryBuilder\Condition\OrCondition; + +use function array_is_list; +use function is_string; /** * @internal */ -abstract class GroupHandler implements QueryHandlerInterface +abstract class GroupHandler extends AbstractHandler { - public function getCondition(array $operands, Context $context): ?array + protected function normalizeCriteria(array $criteria, CriteriaHandler $criteriaHandler): array { - if (!array_key_exists(0, $operands)) { - throw new LogicException( - sprintf( - 'Not found parameter for the "%s" operator.', - $this->getOperator(), - ) - ); - } - if (!is_array($operands[0])) { - throw new LogicException( - sprintf( - 'The parameter for "%s" operator must be an array. Got %s.', - $this->getOperator(), - get_debug_type($operands[0]) - ) - ); + [$operator, $subCriteria] = $this->splitCriteria($criteria); + $normalized = [$operator]; + + foreach ($subCriteria as $value) { + if (!is_array($value)) { + continue; + } + + if (is_string($value[0]) && $tmp = $this->normalizeCondition($value, $criteriaHandler)) { + $normalized[] = $tmp; + } elseif (is_array($value[0])) { + foreach ($value as $val) { + if (is_array($val) && $tmp = $this->normalizeCondition($val, $criteriaHandler)) { + $normalized[] = $tmp; + } + } + } } - if (empty($operands[0])) { + + return $normalized; + } + + private function normalizeCondition(array $value, CriteriaHandler $criteriaHandler): array|ExpressionInterface|null + { + if (!array_is_list($value) || + count($value) < 2 || + !is_string($value[0]) || + !$criteriaHandler->hasHandler($value[0]) + ) { return null; } - $condition = [strtoupper($this->getOperator())]; - foreach ($operands[0] as $subCriteria) { - if (!is_array($subCriteria)) { - throw new LogicException('Incorrect sub-criteria.'); - } - $condition[] = $context->handleCriteria($subCriteria); + + return $criteriaHandler->getHandlerByOperator($value[0]) + ->getCondition($value, $criteriaHandler); + } + + /** + * @param array $criteria + * @param CriteriaHandler $criteriaHandler + * @return array|ExpressionInterface|null + * @throws \Yiisoft\Db\Exception\InvalidArgumentException + */ + public function getCondition(array $criteria, CriteriaHandler $criteriaHandler): array|ExpressionInterface|null + { + if ($criteria === []) { + return null; } - return $condition; + + [$operator, $criteria] = $this->splitCriteria( + $this->normalizeCriteria($criteria, $criteriaHandler) + ); + + return match($operator) { + 'AND' => new AndCondition($criteria), + 'OR' => new OrCondition($criteria), + default => parent::getCondition([$operator, ...$criteria], $criteriaHandler), + }; } } diff --git a/src/FilterHandler/ILikeHandler.php b/src/FilterHandler/ILikeHandler.php new file mode 100644 index 0000000..e596626 --- /dev/null +++ b/src/FilterHandler/ILikeHandler.php @@ -0,0 +1,18 @@ +getOperator()) + ); + } + + if (!isset($criteria[1])) { + throw new LogicException('"Not" criteria must be set.'); } - $subCondition = $context->handleCriteria($operands[0]); - - if (isset($subCondition[0]) && is_string($subCondition[0])) { - $convert = [ - 'IS' => 'IS NOT', - 'IN' => 'NOT IN', - 'EXISTS' => 'NOT EXISTS', - 'BETWEEN' => 'NOT BETWEEN', - 'LIKE' => 'NOT LIKE', - 'ILIKE' => 'NOT ILIKE', - '>' => '<=', - '>=' => '<', - '<' => '>=', - '<=' => '>', - '=' => '!=', - ]; - $operator = strtoupper($subCondition[0]); - if (isset($convert[$operator])) { - $subCondition[0] = $convert[$operator]; - return $subCondition; - } + + if (!is_array($criteria[1]) || !array_is_list($criteria[1]) || $criteria[1] === []) { + throw new LogicException( + sprintf('"Not" criteria must be a non zero list. "%s" given.', get_debug_type($criteria[1])) + ); + } + + [$operator, $subCriteria] = $this->splitCriteria($criteria[1]); + $subOperator = match($operator) { + 'IS' => 'IS NOT', + 'IN' => 'NOT IN', + 'EXISTS' => 'NOT EXISTS', + 'BETWEEN' => 'NOT BETWEEN', + 'LIKE' => 'NOT LIKE', + 'ILIKE' => 'NOT ILIKE', + '>' => '<=', + '>=' => '<', + '<' => '>=', + '<=' => '>', + '=' => '!=', + default => $operator, + }; + + if ($operator !== $subOperator) { + return parent::getCondition([$subOperator, ...$subCriteria], $criteriaHandler); } - return ['NOT', $subCondition]; + return parent::getCondition($criteria, $criteriaHandler); } } diff --git a/src/FilterHandler/OrILikeHandler.php b/src/FilterHandler/OrILikeHandler.php new file mode 100644 index 0000000..d4166b7 --- /dev/null +++ b/src/FilterHandler/OrILikeHandler.php @@ -0,0 +1,19 @@ +format($this->dateTimeFormat); - } - - throw new RuntimeException('Invalid value.'); - } - - public function toScalarOrNull(mixed $value): bool|string|null|int|float - { - if ($value === null) { - return null; - } - return $this->toScalar($value); - } -} diff --git a/src/ValueNormalizerInterface.php b/src/ValueNormalizerInterface.php deleted file mode 100644 index 7aecf7b..0000000 --- a/src/ValueNormalizerInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -getHandlerByOperator($filter); + $byOperator = $criteriaHandler->getHandlerByOperator($filter::getOperator()); + + self::assertInstanceOf(QueryHandlerInterface::class, $byFilter); + self::assertTrue($byOperator === $byFilter); + } + } + + public function testWithHandlers(): void + { + $criteriaHandler = new CriteriaHandler(); + $testHandler = new class implements QueryHandlerInterface { + public function getOperator(): string + { + return 'test-handler'; + } + + public function getCondition(array $criteria, $criteriaHandler): array|ExpressionInterface|null + { + return $criteria; + } + + public function applyFilter(QueryInterface $query, FilterInterface $filter, $criteriaHandler): QueryInterface + { + return $query; + } + + public function applyHaving(QueryInterface $query, FilterInterface $filter, $criteriaHandler): QueryInterface + { + return $query; + } + }; + + $newCriteriaHandler = $criteriaHandler->withFilterHandlers($testHandler); + + self::assertFalse($criteriaHandler === $newCriteriaHandler); + self::assertInstanceOf(QueryHandlerInterface::class, $newCriteriaHandler->getHandlerByOperator('test-handler')); + self::assertInstanceOf(QueryHandlerInterface::class, $newCriteriaHandler->getHandlerByOperator('=')); + + $this->expectException(LogicException::class); + + $criteriaHandler->getHandlerByOperator('test-handler'); + } + + public function testHasHandler(): void + { + $criteriaHandler = new CriteriaHandler(); + + self::assertTrue($criteriaHandler->hasHandler('=')); + self::assertTrue($criteriaHandler->hasHandler(new Equals('column', 1))); + self::assertFalse($criteriaHandler->hasHandler('foo')); + } +} diff --git a/tests/DataFilterTest.php b/tests/DataFilterTest.php index 9ec2cb8..29066e1 100644 --- a/tests/DataFilterTest.php +++ b/tests/DataFilterTest.php @@ -7,10 +7,14 @@ use PHPUnit\Framework\TestCase; use Yiisoft\Data\Db\QueryDataReader; use Yiisoft\Data\Db\Tests\Support\TestTrait; +use Yiisoft\Data\Reader\Filter\All; +use Yiisoft\Data\Reader\Filter\Any; use Yiisoft\Data\Reader\Filter\Between; use Yiisoft\Data\Reader\Filter\Equals; +use Yiisoft\Data\Reader\Filter\EqualsEmpty; use Yiisoft\Data\Reader\Filter\GreaterThan; use Yiisoft\Data\Reader\Filter\GreaterThanOrEqual; +use Yiisoft\Data\Reader\Filter\Group; use Yiisoft\Data\Reader\Filter\In; use Yiisoft\Data\Reader\Filter\LessThan; use Yiisoft\Data\Reader\Filter\LessThanOrEqual; @@ -23,7 +27,7 @@ final class DataFilterTest extends TestCase { use TestTrait; - public function simpleDataProvider(): array + public static function simpleDataProvider(): array { return [ 'equals' => [ @@ -58,6 +62,10 @@ public function simpleDataProvider(): array new Like('column', 'foo'), "[column] LIKE '%foo%'", ], + 'equals-empty' => [ + new EqualsEmpty('column'), + "[column] IS NULL", + ], ]; } @@ -81,7 +89,7 @@ public function testSimpleFilter(FilterInterface $filter, string $condition): vo ); } - public function notDataProvider(): array + public static function notDataProvider(): array { return [ 'equals' => [ @@ -138,4 +146,55 @@ public function testNotFilter(FilterInterface $filter, string $condition): void $expected, ); } + + public static function groupFilterDataProvider(): array + { + return [ + 'all' => [ + new All( + new Equals('equals', 1), + new GreaterThanOrEqual('greater', 10), + new Any( + new LessThan('less', 5), + new Like('like', 'foo'), + ), + ), + "([equals] = 1) AND ([greater] >= 10) AND (([less] < 5) OR ([like] LIKE '%foo%'))", + ], + 'any' => [ + new Any( + new LessThan('less', 5), + new Like('like', 'bar'), + new All( + new Equals('equals', 1), + new GreaterThanOrEqual('greater', 10), + ), + ), + "([less] < 5) OR ([like] LIKE '%bar%') OR (([equals] = 1) AND ([greater] >= 10))", + ] + ]; + } + + /** + * @dataProvider groupFilterDataProvider + * @param Group $filter + * @param string $condition + * @return void + */ + public function testGroupFilter(Group $filter, string $condition): void + { + $db = $this->getConnection(); + $query = (new Query($db)) + ->from('customer'); + + $dataReader = (new QueryDataReader($query)) + ->withFilter($filter); + + $expected = 'SELECT * FROM [customer] WHERE ' . $condition; + + $this->assertSame( + $expected, + $dataReader->getPreparedQuery()->createCommand()->getRawSql(), + ); + } } diff --git a/tests/DataReaderTest.php b/tests/DataReaderTest.php index f2a9751..0177eb9 100644 --- a/tests/DataReaderTest.php +++ b/tests/DataReaderTest.php @@ -39,8 +39,9 @@ public function testOffset(): void $query = (new Query($db)) ->from('customer'); - $dataReader = (new QueryDataReader($query)) - ->withOffset(2); + $dataReader = (new QueryDataReader($query))->withOffset(2); + $sameDataReader = $dataReader->withOffset(2); + $newDataReader = $dataReader->withOffset(5); $query->offset(2); $actual = $dataReader->getPreparedQuery()->createCommand()->getRawSql(); @@ -48,6 +49,8 @@ public function testOffset(): void self::assertSame($expected, $actual); self::assertStringEndsWith('OFFSET 2', $actual); + self::assertTrue($dataReader === $sameDataReader); + self::assertNotTrue($dataReader === $newDataReader); } public function testLimit(): void @@ -59,6 +62,8 @@ public function testLimit(): void $dataReader = (new QueryDataReader($query)) ->withOffset(1) ->withLimit(1); + $sameDataReader = $dataReader->withLimit(1); + $newDataReader = $dataReader->withLimit(2); $query ->offset(1) ->limit(1); @@ -68,6 +73,8 @@ public function testLimit(): void self::assertSame($expected, $actual); self::assertStringEndsWith('LIMIT 1 OFFSET 1', $actual); + self::assertTrue($dataReader === $sameDataReader); + self::assertNotTrue($dataReader === $newDataReader); } public function sortDataProvider(): array @@ -155,13 +162,17 @@ public function testDtoCreateItem(): void $query = new CustomerQuery($this->getConnection()); $dataReader = (new CustomerDataReader($query)) ->withBatchSize(null); - + $sameDataReader = $dataReader->withBatchSize(null); + $newDataReader = $dataReader->withBatchSize(10); self::assertInstanceOf(CustomerDTO::class, $dataReader->readOne()); - foreach ($dataReader->read() as $row) { + foreach ($sameDataReader->read() as $row) { self::assertInstanceOf(CustomerDTO::class, $row); } + + self::assertTrue($dataReader === $sameDataReader); + self::assertNotTrue($dataReader === $newDataReader); } public function testObjectCreateItem(): void diff --git a/tests/DbDataFilterTest.php b/tests/DbDataFilterTest.php new file mode 100644 index 0000000..1f0d2ac --- /dev/null +++ b/tests/DbDataFilterTest.php @@ -0,0 +1,421 @@ + [ + new Equals('equals', 1), + '[equals] = 1', + ], + 'equals-null' => [ + new Equals('equals', null), + '[equals] IS NULL', + ], + 'equals-in' => [ + new Equals('equals', [1,2,3]), + '[equals] IN (1, 2, 3)', + ], + 'equals-expression' => [ + new Equals(new Expression('[[json]] #> [[key]]'), 1), + '[json] #> [key] = 1', + ], + 'between' => [ + new Between('column', 100, 200), + '[column] BETWEEN 100 AND 200', + ], + 'between-wo-min' => [ + new Between('column', null, 200), + '[column] <= 200', + ], + 'between-wo-max' => [ + new Between('column', 100, null), + '[column] >= 100', + ], + 'between-empty' => [ + new Between('column', null, ''), + '', + ], + 'greater-than' => [ + new GreaterThan('column', 10), + '[column] > 10', + ], + 'greater-than-or-equal' => [ + new GreaterThanOrEqual('column', 20), + '[column] >= 20', + ], + 'less-than' => [ + new LessThan('column', 50), + '[column] < 50', + ], + 'less-than-or-equal' => [ + new LessThanOrEqual('column', 75), + '[column] <= 75', + ], + 'in' => [ + new In('id', [1, 2, 3, 5, 4]), + '[id] IN (1, 2, 3, 5, 4)', + ], + 'like' => [ + new Like('text', 'ex'), + "[text] LIKE '%ex%'", + ], + 'like-expression' => [ + new Like( + new Expression('json #> :columns'), + new Expression("CONCAT('%', json -> :column, '%')"), + [ + ':columns' => '{foo, bar}', + ':column' => 'text-column', + ] + ), + "json #> '{foo, bar}' LIKE CONCAT('%', json -> 'text-column', '%')", + ], + 'like-wo-start' => [ + (new Like('text', 'te'))->withoutStart(), + "[text] LIKE 'te%'", + ], + 'like-wo-end' => [ + (new Like('text', 'xt'))->withoutEnd(), + "[text] LIKE '%xt'", + ], + 'like-wo-both' => [ + (new Like('text', 'text'))->withoutBoth(), + "[text] LIKE 'text'", + ], + 'like-with-both' => [ + (new Like('text', 'text'))->withoutBoth()->withBoth(), + "[text] LIKE '%text%'", + ], + 'ilike' => [ + new ILike('TEXT', 'ex'), + "[TEXT] ILIKE '%ex%'", + ], + 'ilike-wo-start' => [ + (new ILike('TEXT', 'te'))->withoutStart(), + "[TEXT] ILIKE 'te%'", + ], + 'ilike-wo-end' => [ + (new ILike('TEXT', 'xt'))->withoutEnd(), + "[TEXT] ILIKE '%xt'", + ], + 'ilike-wo-both' => [ + (new ILike(new Expression('[[TEXT]]'), 'text'))->withoutBoth(), + "[TEXT] ILIKE 'text'", + ], + 'or-like' => [ + new OrLike('text', ['te', 'xt']), + "[text] LIKE '%te%' OR [text] LIKE '%xt%'", + ], + 'or-ilike' => [ + new OrILike(new Expression("CONCAT_WS(' ', [[column_1]], [[column_2]])"), ['te', 'xt']), + "CONCAT_WS(' ', [column_1], [column_2]) ILIKE '%te%' OR CONCAT_WS(' ', [column_1], [column_2]) ILIKE '%xt%'", + ], + 'equals-empty' => [ + new EqualsEmpty('empty_column', '', '0'), + "([empty_column] IS NULL) OR ([empty_column] IN ('', '0'))", + ], + ]; + } + + /** + * @dataProvider simpleDataProvider + */ + public function testSimpleFilter(FilterInterface $filter, string $condition): void + { + $db = $this->getConnection(); + $query = (new Query($db)) + ->from('customer'); + + $dataReader = (new QueryDataReader($query)) + ->withFilter($filter); + + if ($condition) { + $expected = 'SELECT * FROM [customer] WHERE ' . $condition; + } else { + $expected = 'SELECT * FROM [customer]'; + } + + self::assertSame( + $expected, + $dataReader->getPreparedQuery()->createCommand()->getRawSql(), + ); + } + + public function testExistsFilter(): void + { + $db = $this->getConnection(); + $subQuery = (new Query($db)) + ->select(new Expression('1')) + ->from('some_table'); + + $filter = new Exists($subQuery); + + $query = (new Query($db)) + ->from('customer'); + + $dataReader = (new QueryDataReader($query)) + ->withFilter($filter); + + $expected = 'SELECT * FROM [customer] WHERE EXISTS (SELECT 1 FROM [some_table])'; + + $this->assertSame( + $expected, + $dataReader->getPreparedQuery()->createCommand()->getRawSql(), + ); + + $expected = 'SELECT * FROM [customer] WHERE NOT EXISTS (SELECT 1 FROM [some_table])'; + $query = (new Query($db)) + ->from('customer'); + $dataReader = (new QueryDataReader($query)) + ->withFilter(new Not($filter)); + + self::assertSame( + $expected, + $dataReader->getPreparedQuery()->createCommand()->getRawSql(), + ); + } + + public static function groupFilterDataProvider(): array + { + return [ + 'all' => [ + new All( + new Equals('equals', 2), + new Any( + new LessThan('less', 10), + new Like('like', 'bar'), + ), + ), + "([equals] = 2) AND (([less] < 10) OR ([like] LIKE '%bar%'))", + ], + 'any' => [ + new Any( + new Equals('equals', 5), + new In('in_column', ['foo', 'bar']), + new All( + new GreaterThanOrEqual('greater', 10), + new Not(new Like('like', 'foo')), + ), + ), + "([equals] = 5) OR ([in_column] IN ('foo', 'bar')) OR (([greater] >= 10) AND ([like] NOT LIKE '%foo%'))", + ], + ]; + } + + /** + * @dataProvider groupFilterDataProvider + * @param FilterInterface $filter + * @param string $condition + * @return void + * @throws \Yiisoft\Db\Exception\Exception + * @throws \Yiisoft\Db\Exception\InvalidConfigException + */ + public function testGroupFilter(FilterInterface $filter, string $condition): void + { + $db = $this->getConnection(); + $query = (new Query($db))->from('customer'); + $dataReader = (new QueryDataReader($query))->withFilter($filter); + $expected = 'SELECT * FROM [customer] WHERE ' . $condition; + + self::assertSame( + $expected, + $dataReader->getPreparedQuery()->createCommand()->getRawSql(), + ); + } + + public function testEqualsEmptyException(): void + { + new EqualsEmpty('column', 0, false, ''); + self::assertTrue(true); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('$value must be equal php "empty". "empty" given.'); + + new EqualsEmpty('column', 'empty'); + } + + public function testInQueryFilter(): void + { + $db = $this->getConnection(); + $query = (new Query($db))->from('customer'); + $inQuery = (new Query($db))->select('id')->from('other_table')->where('group_id = :group'); + $filter = new In('id', $inQuery, [ + ':group' => 10, + ]); + + $dataReader = (new QueryDataReader($query))->withFilter($filter); + $expected = 'SELECT * FROM [customer] WHERE [id] IN (SELECT [id] FROM [other_table] WHERE group_id = 10)'; + + self::assertSame( + $expected, + $dataReader->getPreparedQuery()->createCommand()->getRawSql(), + ); + } + + public static function nullableFilterDataProvider(): array + { + return [ + [ + new Equals('column', null), + ['IS', 'column', null], + ], + [ + new GreaterThan('greater', null), + ['IS', 'greater', null], + ], + [ + new GreaterThanOrEqual('greater-or-equal', null), + ['IS', 'greater-or-equal', null], + ], + [ + new LessThan('less-than', null), + ['IS', 'less-than', null], + ], + [ + new LessThanOrEqual('less-than-or-equal', null), + ['IS', 'less-than-or-equal', null], + ], + [ + new Like('text', null), + ['IS', 'text', null], + ], + ]; + } + + /** + * @dataProvider nullableFilterDataProvider + * @param CompareFilter $filter + * @param array $criteria + * @return void + */ + public function testIgnoreNull(CompareFilter $filter, array $criteria): void + { + $ignoreFilter = $filter->withIgnoreNull(); + $sameFilter = $filter->withIgnoreNull(false); + + self::assertTrue($sameFilter === $filter); + self::assertFalse($ignoreFilter === $filter); + self::assertSame([], $ignoreFilter->toCriteriaArray()); + self::assertSame($criteria, $filter->toCriteriaArray()); + } + + public function testGroupWithCriteria(): void + { + $filter = new All(); + $newFilter = $filter->withCriteriaArray([ + ['>', 'test', 1], + ['<', 'test', 5], + ]); + + + self::assertFalse($filter === $newFilter); + self::assertSame( + [ + 'and', + ['>', 'test', 1], + ['<', 'test', 5] + ], + $newFilter->toCriteriaArray() + ); + } + + public static function groupCriteriaExceptionDataProvider(): array + { + return [ + [ + ['OR', ['>', 'test', 1]], + InvalidArgumentException::class, + 'Invalid filter on "0" key.', + ], + [ + [ + 'ALL' => [ + 'test' => ['<', 5], + ], + ], + InvalidArgumentException::class, + 'Invalid filter operator on "ALL" key.' + ] + ]; + } + + /** + * @dataProvider groupCriteriaExceptionDataProvider + * @param array $criteria + * @param string $exception + * @param string $message + * @return void + */ + public function testGroupWithCriteriaExceptions(array $criteria, string $exception, string $message): void + { + $this->expectException($exception); + $this->expectExceptionMessage($message); + + All::fromCriteriaArray($criteria); + } + + public function testMatchFilter(): void + { + $filter = new Like('column', 0.3); + $withoutEnd = $filter->withoutEnd(); + $withoutStart = $filter->withoutStart(); + + self::assertFalse($filter === $withoutEnd); + self::assertTrue($filter === $filter->withEnd()); + self::assertFalse($filter === $withoutEnd->withEnd()); + self::assertFalse($filter === $withoutStart); + self::assertTrue($filter === $filter->withStart()); + self::assertFalse($filter === $withoutStart->withStart()); + self::assertTrue($filter === $filter->withBoth()); + self::assertFalse($filter === $filter->withoutBoth()); + self::assertSame( + ['like', 'column', 0.3], + $filter->toCriteriaArray(), + ); + self::assertSame( + ['like', 'column', '%0.3', null], + $filter->withoutEnd()->toCriteriaArray(), + ); + self::assertSame( + ['like', 'column', '0.3%', null], + $filter->withoutStart()->toCriteriaArray(), + ); + self::assertSame( + ['like', 'column', 0.3, null], + $filter->withoutBoth()->toCriteriaArray(), + ); + } +} diff --git a/tests/HandlerTest.php b/tests/HandlerTest.php new file mode 100644 index 0000000..3cf889a --- /dev/null +++ b/tests/HandlerTest.php @@ -0,0 +1,130 @@ + 'bar', 'bar' => 'foo'], + LogicException::class, + 'Incorrect criteria for the "test-handler" operator.', + ], + [ + ['foo'], + LogicException::class, + 'Incorrect criteria for the "test-handler" operator.', + ], + [ + [10, 'foo', 'var'], + InvalidArgumentException::class, + '$operator must be type of "string". "int" given.', + ], + ]; + } + + /** + * @dataProvider handlerConditionExceptionProvider + * @param array $criteria + * @param string $exception + * @param string $message + * @return void + */ + public function testHandlerConditionException(array $criteria, string $exception, string $message): void + { + $criteriaHandler = new CriteriaHandler(); + $handler = new class extends AbstractHandler { + + public function getOperator(): string + { + return 'test-handler'; + } + }; + + $this->expectException($exception); + $this->expectExceptionMessage($message); + + $handler->getCondition($criteria, $criteriaHandler); + } + + public function testEmptyCriteria(): void + { + $criteriaHandler = new CriteriaHandler(); + $handler = new class extends AbstractHandler { + + public function getOperator(): string + { + return 'test-handler'; + } + }; + + $allHandler = new AllHandler(); + $anyHandler = new AnyHandler(); + + self::assertTrue($handler->getCondition([], $criteriaHandler) === null); + self::assertTrue($allHandler->getCondition([], $criteriaHandler) === null); + self::assertTrue($anyHandler->getCondition([], $criteriaHandler) === null); + } + + public static function notHandlerExceptionProvider(): array + { + return [ + [ + ['foo' => 'bar'], + LogicException::class, + 'Incorrect criteria for the "not" operator.', + ], + [ + ['not'], + LogicException::class, + '"Not" criteria must be set.', + ], + [ + ['not', 'foo', 'bar'], + LogicException::class, + '"Not" criteria must be a non zero list. "string" given.', + ], + [ + ['not', ['foo' => 'bar']], + LogicException::class, + '"Not" criteria must be a non zero list. "array" given.', + ], + [ + ['not', []], + LogicException::class, + '"Not" criteria must be a non zero list. "array" given.', + ], + ]; + } + + /** + * @dataProvider notHandlerExceptionProvider + * @param array $criteria + * @param string $exception + * @param string $message + * @return void + */ + public function testNotHandlerExceptions(array $criteria, string $exception, string $message): void + { + $criteriaHandler = new CriteriaHandler(); + $handler = new NotHandler(); + + $this->expectException($exception); + $this->expectExceptionMessage($message); + + $handler->getCondition($criteria, $criteriaHandler); + } +} diff --git a/tests/QueryParamsTest.php b/tests/QueryParamsTest.php new file mode 100644 index 0000000..43c6ec9 --- /dev/null +++ b/tests/QueryParamsTest.php @@ -0,0 +1,95 @@ + [ + (new Equals('equals', new Expression(':param')))->withParam(':param', 'test'), + "[equals] = 'test'", + ], + 'between' => [ + new Between('column', new Expression(':min'), new Expression(':max'), [ + ':min' => 100, + ':max' => 200, + ]), + '[column] BETWEEN 100 AND 200', + ], + 'between-with' => [ + (new Between('column', new Expression(':min'), new Expression(':max')))->withParams([ + ':min' => 300, + ':max' => 400, + ]), + '[column] BETWEEN 300 AND 400', + ], + 'all' => [ + new All( + (new Equals('equals', new Expression(':param')))->withParam(':param', '1'), + new Between('column', new Expression(':min'), new Expression(':max'), [ + ':min' => 10, + ':max' => 20, + ]), + new Like('text', new Expression(':like'), [':like' => '%foo-bar']), + ), + "([equals] = '1') AND ([column] BETWEEN 10 AND 20) AND ([text] LIKE '%foo-bar')", + ], + ]; + } + + /** + * @dataProvider paramsDataProvider + * @param QueryFilterInterface $filter + * @param string $condition + * @return void + * @throws \Yiisoft\Db\Exception\Exception + * @throws \Yiisoft\Db\Exception\InvalidConfigException + */ + public function testParams(QueryFilterInterface $filter, string $condition): void + { + $db = $this->getConnection(); + $query = (new Query($db))->from('customer'); + $dataReader = (new QueryDataReader($query))->withFilter($filter); + $expected = 'SELECT * FROM [customer] WHERE ' . $condition; + + self::assertSame( + $expected, + $dataReader->getPreparedQuery()->createCommand()->getRawSql(), + ); + } + + public function testParamsTrait(): void + { + $object = new class { + use ParamsTrait; + }; + + $asArray = $object->withParams(['foo' => 'bar']); + $asParam = $asArray->withParam('foo', 'test'); + + self::assertFalse($object === $asArray); + self::assertSame(['foo' => 'bar'], $asArray->getParams()); + self::assertEmpty($object->getParams()); + self::assertFalse($object === $asParam); + self::assertFalse($asArray === $asParam); + self::assertSame(['foo' => 'test'], $asParam->getParams()); + } +} diff --git a/tests/QueryWithFiltersTest.php b/tests/QueryWithFiltersTest.php index 181b063..e654914 100644 --- a/tests/QueryWithFiltersTest.php +++ b/tests/QueryWithFiltersTest.php @@ -35,7 +35,7 @@ public function simpleDataProvider(): array '[equals] = 1', ], 'equals-datetime' => [ - new Equals('column', new DateTime('2011-01-01T15:03:01.012345Z')), + new Equals('column', (new DateTime('2011-01-01T15:03:01.012345Z'))->format('Y-m-d H:i:s')), "[column] = '2011-01-01 15:03:01'", ], 'between' => [ @@ -43,7 +43,7 @@ public function simpleDataProvider(): array '[column] BETWEEN 100 AND 300', ], 'between-dates' => [ - new Between('column', new DateTime('2011-01-01T15:00:01'), new DateTime('2011-01-01T15:10:01')), + new Between('column', (new DateTime('2011-01-01T15:00:01'))->format('Y-m-d H:i:s'), (new DateTime('2011-01-01T15:10:01'))->format('Y-m-d H:i:s')), "[column] BETWEEN '2011-01-01 15:00:01' AND '2011-01-01 15:10:01'", ], 'greater-than' => [ @@ -51,7 +51,7 @@ public function simpleDataProvider(): array '[column] > 1000', ], 'greater-than-date' => [ - new GreaterThan('column', new DateTime('2011-01-01T15:00:01')), + new GreaterThan('column', (new DateTime('2011-01-01T15:00:01'))->format('Y-m-d H:i:s')), "[column] > '2011-01-01 15:00:01'", ], [ @@ -112,8 +112,8 @@ public function testSimpleHaving(FilterInterface $having, string $condition): vo $expected = 'SELECT * FROM [customer] HAVING ' . $condition; $this->assertSame( - $dataReader->getPreparedQuery()->createCommand()->getRawSql(), $expected, + $dataReader->getPreparedQuery()->createCommand()->getRawSql(), ); }