diff --git a/src/Collection/Expression/LikeExpression.php b/src/Collection/Expression/LikeExpression.php new file mode 100644 index 00000000..57e66293 --- /dev/null +++ b/src/Collection/Expression/LikeExpression.php @@ -0,0 +1,81 @@ +input = $input; + $this->mode = $mode; + } + + + public function getInput(): string + { + return $this->input; + } + + + public function getMode(): int + { + return $this->mode; + } +} diff --git a/src/Collection/Functions/CompareLikeFunction.php b/src/Collection/Functions/CompareLikeFunction.php new file mode 100644 index 00000000..4625c6cb --- /dev/null +++ b/src/Collection/Functions/CompareLikeFunction.php @@ -0,0 +1,118 @@ +getValue($entity, $args[0]); + + $likeExpression = $args[1]; + assert($likeExpression instanceof LikeExpression); + $mode = $likeExpression->getMode(); + + if ($valueReference->propertyMetadata !== null) { + $targetValue = $helper->normalizeValue($likeExpression->getInput(), $valueReference->propertyMetadata, true); + } else { + $targetValue = $likeExpression->getInput(); + } + + if ($valueReference->isMultiValue) { + foreach ($valueReference->value as $subValue) { + if ($this->evaluateInPhp($mode, $subValue, $targetValue)) { + return true; + } + } + return false; + } else { + return $this->evaluateInPhp($mode, $valueReference->value, $targetValue); + } + } + + + public function processQueryBuilderExpression( + DbalQueryBuilderHelper $helper, + QueryBuilder $builder, + array $args + ): DbalExpressionResult + { + assert(count($args) === 2); + + $expression = $helper->processPropertyExpr($builder, $args[0]); + + $likeExpression = $args[1]; + assert($likeExpression instanceof LikeExpression); + $mode = $likeExpression->getMode(); + + if ($expression->valueNormalizer !== null) { + $cb = $expression->valueNormalizer; + $value = $cb($likeExpression->getInput()); + } else { + $value = $likeExpression->getInput(); + } + + return $this->evaluateInDb($mode, $expression, $value); + } + + + /** + * @param mixed $sourceValue + * @param mixed $targetValue + */ + protected function evaluateInPhp(int $mode, $sourceValue, $targetValue): bool + { + if ($mode === LikeExpression::MODE_RAW) { + $regexp = '~^' . preg_quote($targetValue, '~') . '$~'; + $regexp = str_replace(['_', '%'], ['.', '.*'], $regexp); + return Strings::match($sourceValue, $regexp) !== null; + + } elseif ($mode === LikeExpression::MODE_STARTS_WITH) { + return Strings::startsWith($sourceValue, $targetValue); + + } elseif ($mode === LikeExpression::MODE_ENDS_WITH) { + return Strings::endsWith($sourceValue, $targetValue); + + } elseif ($mode === LikeExpression::MODE_CONTAINS) { + $regexp = '~^.*' . preg_quote($targetValue, '~') . '.*$~'; + return Strings::match($sourceValue, $regexp) !== null; + + } else { + throw new InvalidStateException(); + } + } + + + /** + * @param mixed $value + */ + protected function evaluateInDb(int $mode, DbalExpressionResult $expression, $value): DbalExpressionResult + { + if ($mode === LikeExpression::MODE_RAW) { + return $expression->append('LIKE %s', $value); + } elseif ($mode === LikeExpression::MODE_STARTS_WITH) { + return $expression->append('LIKE %like_', $value); + } elseif ($mode === LikeExpression::MODE_ENDS_WITH) { + return $expression->append('LIKE %_like', $value); + } elseif ($mode === LikeExpression::MODE_CONTAINS) { + return $expression->append('LIKE %_like_', $value); + } else { + throw new InvalidStateException(); + } + } +} diff --git a/src/Collection/Helpers/ConditionParserHelper.php b/src/Collection/Helpers/ConditionParserHelper.php index 7201d5ca..e9bbc504 100644 --- a/src/Collection/Helpers/ConditionParserHelper.php +++ b/src/Collection/Helpers/ConditionParserHelper.php @@ -6,6 +6,7 @@ use Nextras\Orm\Collection\Functions\CompareEqualsFunction; use Nextras\Orm\Collection\Functions\CompareGreaterThanEqualsFunction; use Nextras\Orm\Collection\Functions\CompareGreaterThanFunction; +use Nextras\Orm\Collection\Functions\CompareLikeFunction; use Nextras\Orm\Collection\Functions\CompareNotEqualsFunction; use Nextras\Orm\Collection\Functions\CompareSmallerThanEqualsFunction; use Nextras\Orm\Collection\Functions\CompareSmallerThanFunction; @@ -27,7 +28,7 @@ class ConditionParserHelper */ public static function parsePropertyOperator(string $condition): array { - if (!preg_match('#^(.+?)(!=|<=|>=|=|>|<)?$#', $condition, $matches)) { + if (!preg_match('#^(.+?)(!=|<=|>=|=|>|<|~)?$#', $condition, $matches)) { return [CompareEqualsFunction::class, $condition]; } $operator = $matches[2] ?? '='; @@ -43,6 +44,8 @@ public static function parsePropertyOperator(string $condition): array return [CompareSmallerThanEqualsFunction::class, $matches[1]]; } elseif ($operator === '<') { return [CompareSmallerThanFunction::class, $matches[1]]; + } elseif ($operator === '~') { + return [CompareLikeFunction::class, $matches[1]]; } else { throw new InvalidStateException(); } diff --git a/src/Repository/Repository.php b/src/Repository/Repository.php index 751645ad..6983846a 100644 --- a/src/Repository/Repository.php +++ b/src/Repository/Repository.php @@ -14,6 +14,7 @@ use Nextras\Orm\Collection\Functions\CompareEqualsFunction; use Nextras\Orm\Collection\Functions\CompareGreaterThanEqualsFunction; use Nextras\Orm\Collection\Functions\CompareGreaterThanFunction; +use Nextras\Orm\Collection\Functions\CompareLikeFunction; use Nextras\Orm\Collection\Functions\CompareNotEqualsFunction; use Nextras\Orm\Collection\Functions\CompareSmallerThanEqualsFunction; use Nextras\Orm\Collection\Functions\CompareSmallerThanFunction; @@ -280,6 +281,7 @@ protected function createCollectionFunction(string $name) CompareNotEqualsFunction::class => true, CompareSmallerThanEqualsFunction::class => true, CompareSmallerThanFunction::class => true, + CompareLikeFunction::class => true, ConjunctionOperatorFunction::class => true, DisjunctionOperatorFunction::class => true, AvgAggregateFunction::class => true, diff --git a/tests/cases/integration/Collection/collection.customFunctions.phpt b/tests/cases/integration/Collection/collection.customFunctions.phpt deleted file mode 100644 index b9e93e04..00000000 --- a/tests/cases/integration/Collection/collection.customFunctions.phpt +++ /dev/null @@ -1,110 +0,0 @@ -section === Helper::SECTION_ARRAY) { - Environment::skip('Test only DbalMapper'); - } - - $count = $this->orm->books->findBy([LikeFunction::class, 'title', 'Book'])->count(); - Assert::same(4, $count); - - $count = $this->orm->books->findBy([LikeFunction::class, 'title', 'Book 1'])->count(); - Assert::same(1, $count); - - $count = $this->orm->books->findBy([LikeFunction::class, 'title', 'Book X'])->count(); - Assert::same(0, $count); - } - - - public function testFilterLikeCombined() - { - if ($this->section === Helper::SECTION_ARRAY) { - Environment::skip('Test only DbalMapper'); - } - - $count = $this->orm->books->findBy([ - ICollection::AND, - [LikeFunction::class, 'title', 'Book'], - ['translator!=' => null], - ])->count(); - Assert::same(3, $count); - - - $count = $this->orm->books->findBy([ - ICollection::OR, - [LikeFunction::class, 'title', 'Book 1'], - ['translator' => null], - ])->count(); - Assert::same(2, $count); - } - - - public function testFilterLikeArray() - { - if ($this->section === Helper::SECTION_ARRAY) { - Environment::skip('Test only DbalMapper'); - } - - $collection = new ArrayCollection(iterator_to_array($this->orm->books->findAll()), $this->orm->books); - - $count = $collection->findBy([LikeFunction::class, 'title', 'Book'])->count(); - Assert::same(4, $count); - - $count = $collection->findBy([LikeFunction::class, 'title', 'Book 1'])->count(); - Assert::same(1, $count); - - $count = $collection->findBy([LikeFunction::class, 'title', 'Book X'])->count(); - Assert::same(0, $count); - } - - - public function testFilterLikeArrayCombined() - { - if ($this->section === Helper::SECTION_ARRAY) { - Environment::skip('Test only DbalMapper'); - } - - $collection = new ArrayCollection(iterator_to_array($this->orm->books->findAll()), $this->orm->books); - - $count = $collection->findBy([ - ICollection::AND, - [LikeFunction::class, 'title', 'Book'], - ['translator!=' => null], - ])->count(); - Assert::same(3, $count); - - - $count = $collection->findBy([ - ICollection::OR, - [LikeFunction::class, 'title', 'Book 1'], - ['translator' => null], - ])->count(); - Assert::same(2, $count); - } -} - - -$test = new CollectionCustomFunctionsTest($dic); -$test->run(); diff --git a/tests/cases/integration/Collection/collection.like.phpt b/tests/cases/integration/Collection/collection.like.phpt new file mode 100644 index 00000000..a03ae5d2 --- /dev/null +++ b/tests/cases/integration/Collection/collection.like.phpt @@ -0,0 +1,88 @@ +orm->books->findBy(['title~' => LikeExpression::raw('Book%')])->count(); + Assert::same(4, $count); + + $count = $this->orm->books->findBy(['title~' => LikeExpression::raw('Book 1%')])->count(); + Assert::same(1, $count); + + $count = $this->orm->books->findBy(['title~' => LikeExpression::raw('Book X%')])->count(); + Assert::same(0, $count); + } + + + public function testFilterLikeCombined() + { + $count = $this->orm->books->findBy([ + ICollection::AND, + ['title~' => LikeExpression::raw('Book%')], + ['translator!=' => null], + ])->count(); + Assert::same(3, $count); + + $count = $this->orm->books->findBy([ + ICollection::OR, + ['title~' => LikeExpression::raw('Book 1%')], + ['translator' => null], + ])->count(); + Assert::same(2, $count); + } + + + public function testFilterLikePositions() + { + $count = $this->orm->books->findBy(['title~' => LikeExpression::startsWith('Book')])->count(); + Assert::same(4, $count); + + $count = $this->orm->books->findBy(['title~' => LikeExpression::startsWith('Book 1')])->count(); + Assert::same(1, $count); + + $count = $this->orm->books->findBy(['title~' => LikeExpression::startsWith('Book X')])->count(); + Assert::same(0, $count); + + + $count = $this->orm->books->findBy(['title~' => LikeExpression::endsWith('ook')])->count(); + Assert::same(0, $count); + + $count = $this->orm->books->findBy(['title~' => LikeExpression::endsWith('ook 1')])->count(); + Assert::same(1, $count); + + $count = $this->orm->books->findBy(['title~' => LikeExpression::endsWith('ook X')])->count(); + Assert::same(0, $count); + + + $count = $this->orm->books->findBy(['title~' => LikeExpression::contains('ook')])->count(); + Assert::same(4, $count); + + $count = $this->orm->books->findBy(['title~' => LikeExpression::contains('ook 1')])->count(); + Assert::same(1, $count); + + $count = $this->orm->books->findBy(['title~' => LikeExpression::contains('ook X')])->count(); + Assert::same(0, $count); + } +} + + +$test = new CollectionLikeTest($dic); +$test->run(); diff --git a/tests/inc/model/LikeFunction.php b/tests/inc/model/LikeFunction.php deleted file mode 100644 index 350d813b..00000000 --- a/tests/inc/model/LikeFunction.php +++ /dev/null @@ -1,41 +0,0 @@ -getValue($entity, $args[0])->value; - return Strings::startsWith($value, $args[1]); - } - - - public function processQueryBuilderExpression( - DbalQueryBuilderHelper $helper, - QueryBuilder $builder, - array $args - ): DbalExpressionResult - { - assert(count($args) === 2 && is_string($args[0]) && is_string($args[1])); - - $expression = $helper->processPropertyExpr($builder, $args[0]); - return $expression->append('LIKE %like_', $args[1]); - } -} diff --git a/tests/inc/model/book/BooksRepository.php b/tests/inc/model/book/BooksRepository.php index 06137c21..e492ed19 100644 --- a/tests/inc/model/book/BooksRepository.php +++ b/tests/inc/model/book/BooksRepository.php @@ -32,14 +32,4 @@ public function findByTags($name) { return $this->findBy(['tags->name' => $name]); } - - - public function createCollectionFunction(string $name) - { - if ($name === LikeFunction::class) { - return new LikeFunction(); - } else { - return parent::createCollectionFunction($name); - } - } }