diff --git a/src/Doctrine/Odm/Filter/PartialSearchFilter.php b/src/Doctrine/Odm/Filter/PartialSearchFilter.php index f85b1a5b32..f5dcba3a13 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 e25491a467..d4bcafd88b 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 2fe846ecae..ade122541c 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 f3533f5980..1fffb74173 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 4dd45a6412..299b6b135c 100644 --- a/tests/Functional/Parameters/PartialSearchFilterTest.php +++ b/tests/Functional/Parameters/PartialSearchFilterTest.php @@ -141,6 +141,44 @@ 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"' => [ + '/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 diff --git a/tests/RecreateSchemaTrait.php b/tests/RecreateSchemaTrait.php index 3808d04c89..80ac15a8b9 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();