From d7a6623e0c4c8f2c1667c44fe663054f93789920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radim=20Vaculi=CC=81k?= Date: Sun, 21 Dec 2025 16:08:22 +0100 Subject: [PATCH] Support for NOT LIKE Closes #745 --- docs/collection-filtering.md | 2 +- src/Collection/Expression/LikeExpression.php | 10 ++++++++++ src/Collection/Functions/CompareLikeFunction.php | 6 ++++++ .../cases/integration/Collection/collection.like.phpt | 7 +++++++ .../CollectionLikeTest_testFilterLikePositions.sql | 2 ++ 5 files changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/collection-filtering.md b/docs/collection-filtering.md index 995d34ae7..52d310a08 100644 --- a/docs/collection-filtering.md +++ b/docs/collection-filtering.md @@ -79,7 +79,7 @@ Filtering over virtual properties is generally unsupported and provides undefine #### LIKE filtering -`LIKE` filtering is supported and directly provided in Nextras Orm. Use the `~` comparison operator. The value must be wrapped as a `Nextras\Orm\Collection\Expression\LikeExpression` instance; use its static builders to create one: choose from `startsWith`, `endsWith`, or `contains`. Alternatively, you may provide your wildcard expression with the `raw` method. Be aware that the `raw` method expects sanitized input. +`LIKE` filtering is supported and directly provided in Nextras Orm. Use the `~` comparison operator. The value must be wrapped as a `Nextras\Orm\Collection\Expression\LikeExpression` instance; use its static builders to create one: choose from `startsWith`, `endsWith`, `contains` or `notContains`. Alternatively, you may provide your wildcard expression with the `raw` method. Be aware that the `raw` method expects sanitized input. ```php // finds all users with email hosted on gmail.com diff --git a/src/Collection/Expression/LikeExpression.php b/src/Collection/Expression/LikeExpression.php index 08b47ae59..b00244421 100644 --- a/src/Collection/Expression/LikeExpression.php +++ b/src/Collection/Expression/LikeExpression.php @@ -48,11 +48,21 @@ public static function contains(string $input): LikeExpression return new self($input, self::MODE_CONTAINS); } + /** + * Wraps input as not contains filter (i.e. string may start and end 0-n other characters). + * Special LIKE characters are sanitized. + */ + public static function notContains(string $input): LikeExpression + { + return new self($input, self::MODE_NOT_CONTAINS); + } + public const MODE_RAW = 0; public const MODE_STARTS_WITH = 1; public const MODE_ENDS_WITH = 2; public const MODE_CONTAINS = 3; + public const MODE_NOT_CONTAINS = 4; private function __construct( diff --git a/src/Collection/Functions/CompareLikeFunction.php b/src/Collection/Functions/CompareLikeFunction.php index 961126c89..71f6d2d23 100644 --- a/src/Collection/Functions/CompareLikeFunction.php +++ b/src/Collection/Functions/CompareLikeFunction.php @@ -107,6 +107,10 @@ protected function evaluateInPhp(int $mode, $sourceValue, $targetValue): bool $regexp = '~^.*' . preg_quote($targetValue, '~') . '.*$~'; return Strings::match($sourceValue, $regexp) !== null; + } elseif ($mode === LikeExpression::MODE_NOT_CONTAINS) { + $regexp = '~^.*' . preg_quote($targetValue, '~') . '.*$~'; + return Strings::match($sourceValue, $regexp) === null; + } else { throw new InvalidStateException(); } @@ -126,6 +130,8 @@ protected function evaluateInDb(int $mode, DbalExpressionResult $expression, $va return $expression->append('LIKE %_like', $value); } elseif ($mode === LikeExpression::MODE_CONTAINS) { return $expression->append('LIKE %_like_', $value); + } elseif ($mode === LikeExpression::MODE_NOT_CONTAINS) { + return $expression->append('NOT LIKE %_like_', $value); } else { throw new InvalidStateException(); } diff --git a/tests/cases/integration/Collection/collection.like.phpt b/tests/cases/integration/Collection/collection.like.phpt index c112bea34..36e26b700 100644 --- a/tests/cases/integration/Collection/collection.like.phpt +++ b/tests/cases/integration/Collection/collection.like.phpt @@ -80,6 +80,13 @@ class CollectionLikeTest extends DataTestCase $count = $this->orm->books->findBy(['title~' => LikeExpression::contains('ook X')])->count(); Assert::same(0, $count); + + + $count = $this->orm->books->findBy(['title~' => LikeExpression::notContains('ABC')])->count(); + Assert::same(4, $count); + + $count = $this->orm->books->findBy(['title~' => LikeExpression::notContains('Book')])->count(); + Assert::same(0, $count); } } diff --git a/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionLikeTest_testFilterLikePositions.sql b/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionLikeTest_testFilterLikePositions.sql index 05f6f4cf1..6426c3621 100644 --- a/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionLikeTest_testFilterLikePositions.sql +++ b/tests/sqls/NextrasTests/Orm/Integration/Collection/CollectionLikeTest_testFilterLikePositions.sql @@ -7,3 +7,5 @@ SELECT "books".* FROM "books" AS "books" WHERE "books"."title" LIKE '%ook X'; SELECT "books".* FROM "books" AS "books" WHERE "books"."title" LIKE '%ook%'; SELECT "books".* FROM "books" AS "books" WHERE "books"."title" LIKE '%ook 1%'; SELECT "books".* FROM "books" AS "books" WHERE "books"."title" LIKE '%ook X%'; +SELECT "books".* FROM "books" AS "books" WHERE "books"."title" NOT LIKE '%ABC%'; +SELECT "books".* FROM "books" AS "books" WHERE "books"."title" NOT LIKE '%Book%';