From f9432a08ab5a3523d19fb887eff7c7ba4b72314e Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 14 Jan 2026 19:49:15 +0100 Subject: [PATCH 1/2] Add case sensitive option to PartialSearchFilter --- .../Odm/Filter/PartialSearchFilter.php | 8 ++++-- .../Orm/Filter/PartialSearchFilter.php | 21 ++++++++++---- .../Fixtures/TestBundle/Document/Chicken.php | 4 +++ tests/Fixtures/TestBundle/Entity/Chicken.php | 4 +++ .../Parameters/PartialSearchFilterTest.php | 28 +++++++++++++++++++ 5 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/Doctrine/Odm/Filter/PartialSearchFilter.php b/src/Doctrine/Odm/Filter/PartialSearchFilter.php index f85b1a5b328..f5dcba3a137 100644 --- a/src/Doctrine/Odm/Filter/PartialSearchFilter.php +++ b/src/Doctrine/Odm/Filter/PartialSearchFilter.php @@ -29,6 +29,10 @@ final class PartialSearchFilter implements FilterInterface, OpenApiParameterFilt use BackwardCompatibleFilterDescriptionTrait; use OpenApiFilterTrait; + public function __construct(private readonly bool $caseSensitive = true) + { + } + public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void { $parameter = $context['parameter']; @@ -47,7 +51,7 @@ public function apply(Builder $aggregationBuilder, string $resourceClass, ?Opera if (!is_iterable($values)) { $escapedValue = preg_quote($values, '/'); $match->{$operator}( - $aggregationBuilder->matchExpr()->field($property)->equals(new Regex($escapedValue, 'i')) + $aggregationBuilder->matchExpr()->field($property)->equals(new Regex($escapedValue, $this->caseSensitive ? '' : 'i')) ); return; @@ -60,7 +64,7 @@ public function apply(Builder $aggregationBuilder, string $resourceClass, ?Opera $or->addOr( $aggregationBuilder->matchExpr() ->field($property) - ->equals(new Regex($escapedValue, 'i')) + ->equals(new Regex($escapedValue, $this->caseSensitive ? '' : 'i')) ); } diff --git a/src/Doctrine/Orm/Filter/PartialSearchFilter.php b/src/Doctrine/Orm/Filter/PartialSearchFilter.php index e25491a467c..d4bcafd88b0 100644 --- a/src/Doctrine/Orm/Filter/PartialSearchFilter.php +++ b/src/Doctrine/Orm/Filter/PartialSearchFilter.php @@ -29,6 +29,10 @@ final class PartialSearchFilter implements FilterInterface, OpenApiParameterFilt use BackwardCompatibleFilterDescriptionTrait; use OpenApiFilterTrait; + public function __construct(private readonly bool $caseSensitive = false) + { + } + public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void { $parameter = $context['parameter']; @@ -44,19 +48,26 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q if (!is_iterable($values)) { $parameterName = $queryNameGenerator->generateParameterName($property); - $queryBuilder->setParameter($parameterName, $this->formatLikeValue(strtolower($values))); + $value = $this->caseSensitive ? $values : strtolower($values); + $queryBuilder->setParameter($parameterName, $this->formatLikeValue($value)); - $likeExpression = 'LOWER('.$field.') LIKE :'.$parameterName.' ESCAPE \'\\\''; + $likeExpression = $this->caseSensitive + ? $field.' LIKE :'.$parameterName.' ESCAPE \'\\\'' + : 'LOWER('.$field.') LIKE :'.$parameterName.' ESCAPE \'\\\''; $queryBuilder->{$context['whereClause'] ?? 'andWhere'}($likeExpression); return; } $likeExpressions = []; - foreach ($values as $val) { + foreach ($values as $value) { $parameterName = $queryNameGenerator->generateParameterName($property); - $likeExpressions[] = 'LOWER('.$field.') LIKE :'.$parameterName.' ESCAPE \'\\\''; - $queryBuilder->setParameter($parameterName, $this->formatLikeValue(strtolower($val))); + $likeExpressions[] = $this->caseSensitive + ? $field.' LIKE :'.$parameterName.' ESCAPE \'\\\'' + : 'LOWER('.$field.') LIKE :'.$parameterName.' ESCAPE \'\\\''; + + $val = $this->caseSensitive ? $value : strtolower($value); + $queryBuilder->setParameter($parameterName, $this->formatLikeValue($val)); } $queryBuilder->{$context['whereClause'] ?? 'andWhere'}( diff --git a/tests/Fixtures/TestBundle/Document/Chicken.php b/tests/Fixtures/TestBundle/Document/Chicken.php index 2fe846ecae3..ade122541c3 100644 --- a/tests/Fixtures/TestBundle/Document/Chicken.php +++ b/tests/Fixtures/TestBundle/Document/Chicken.php @@ -33,6 +33,10 @@ filter: new PartialSearchFilter(), property: 'name', ), + 'namePartialSensitive' => new QueryParameter( + filter: new PartialSearchFilter(true), + property: 'name', + ), 'autocomplete' => new QueryParameter(filter: new FreeTextQueryFilter(new OrFilter(new ExactFilter())), properties: ['name', 'ean']), 'q' => new QueryParameter(filter: new FreeTextQueryFilter(new PartialSearchFilter()), properties: ['name', 'ean']), ], diff --git a/tests/Fixtures/TestBundle/Entity/Chicken.php b/tests/Fixtures/TestBundle/Entity/Chicken.php index f3533f59801..1fffb741734 100644 --- a/tests/Fixtures/TestBundle/Entity/Chicken.php +++ b/tests/Fixtures/TestBundle/Entity/Chicken.php @@ -33,6 +33,10 @@ filter: new PartialSearchFilter(), property: 'name', ), + 'namePartialSensitive' => new QueryParameter( + filter: new PartialSearchFilter(true), + property: 'name', + ), 'autocomplete' => new QueryParameter(filter: new FreeTextQueryFilter(new OrFilter(new ExactFilter())), properties: ['name', 'ean']), 'q' => new QueryParameter(filter: new FreeTextQueryFilter(new PartialSearchFilter()), properties: ['name', 'ean']), ], diff --git a/tests/Functional/Parameters/PartialSearchFilterTest.php b/tests/Functional/Parameters/PartialSearchFilterTest.php index 4dd45a64122..88051fb1029 100644 --- a/tests/Functional/Parameters/PartialSearchFilterTest.php +++ b/tests/Functional/Parameters/PartialSearchFilterTest.php @@ -55,6 +55,7 @@ protected function setUp(): void } #[DataProvider('partialSearchFilterProvider')] + #[DataProvider('partialSearchFilterCaseSensitiveProvider')] public function testPartialSearchFilter(string $url, int $expectedCount, array $expectedNames): void { $response = self::createClient()->request('GET', $url); @@ -141,6 +142,33 @@ public static function partialSearchFilterProvider(): \Generator ]; } + public static function partialSearchFilterCaseSensitiveProvider(): \Generator + { + yield 'filter by partial name "tru"' => [ + '/chickens?namePartial=tru', + 1, + ['Gertrude'], + ]; + + yield 'filter by partial name "TRU"' => [ + '/chickens?namePartial=TRU', + 1, + ['Gertrude'], + ]; + + yield 'filter by case sensitive partial name "tru"' => [ + '/chickens?namePartialSensitive=tru', + 1, + ['Gertrude'], + ]; + + yield 'filter by case sensitive partial name "TRU"' => [ + '/chickens?namePartialSensitive=TRU', + 0, + [], + ]; + } + /** * @throws \Throwable * @throws MongoDBException From af6e3089b62cf9cdcd5b4532514fb9fd8db66254 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 23 Jan 2026 16:23:17 +0100 Subject: [PATCH 2/2] Fix --- .../Parameters/PartialSearchFilterTest.php | 12 +++++++++++- tests/RecreateSchemaTrait.php | 10 ++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/Functional/Parameters/PartialSearchFilterTest.php b/tests/Functional/Parameters/PartialSearchFilterTest.php index 88051fb1029..299b6b135ce 100644 --- a/tests/Functional/Parameters/PartialSearchFilterTest.php +++ b/tests/Functional/Parameters/PartialSearchFilterTest.php @@ -55,7 +55,6 @@ protected function setUp(): void } #[DataProvider('partialSearchFilterProvider')] - #[DataProvider('partialSearchFilterCaseSensitiveProvider')] public function testPartialSearchFilter(string $url, int $expectedCount, array $expectedNames): void { $response = self::createClient()->request('GET', $url); @@ -142,6 +141,17 @@ public static function partialSearchFilterProvider(): \Generator ]; } + + #[DataProvider('partialSearchFilterCaseSensitiveProvider')] + public function testPartialSearchCaseSensitiveFilter(string $url, int $expectedCount, array $expectedNames): void + { + if ($this->isMysql() || $this->isSqlite()) { + $this->markTestSkipped('Mysql and sqlite use case insensitive LIKE.'); + } + + $this->testPartialSearchFilter($url, $expectedCount, $expectedNames); + } + public static function partialSearchFilterCaseSensitiveProvider(): \Generator { yield 'filter by partial name "tru"' => [ diff --git a/tests/RecreateSchemaTrait.php b/tests/RecreateSchemaTrait.php index 3808d04c899..80ac15a8b96 100644 --- a/tests/RecreateSchemaTrait.php +++ b/tests/RecreateSchemaTrait.php @@ -78,11 +78,21 @@ private function isMongoDB(): bool return 'mongodb' === static::getContainer()->getParameter('kernel.environment'); } + private function isMysql(): bool + { + return 'mysql' === static::getContainer()->getParameter('kernel.environment'); + } + private function isPostgres(): bool { return 'postgres' === static::getContainer()->getParameter('kernel.environment'); } + private function isSqlite(): bool + { + return \in_array(static::getContainer()->getParameter('kernel.environment'), ['sqlite', 'test'], true); + } + private function getManager(): EntityManagerInterface|DocumentManager { return static::getContainer()->get($this->isMongoDB() ? 'doctrine_mongodb' : 'doctrine')->getManager();