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 a7df955f81..9bfa97699b 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']; @@ -46,7 +50,9 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q $parameterName = $queryNameGenerator->generateParameterName($property); $queryBuilder->setParameter($parameterName, $this->formatLikeValue($values)); - $likeExpression = 'LOWER('.$field.') LIKE LOWER(:'.$parameterName.') ESCAPE \'\\\''; + $likeExpression = $this->caseSensitive + ? $field.' LIKE :'.$parameterName.' ESCAPE \'\\\'' + : 'LOWER('.$field.') LIKE LOWER(:'.$parameterName.') ESCAPE \'\\\''; $queryBuilder->{$context['whereClause'] ?? 'andWhere'}($likeExpression); return; @@ -55,7 +61,10 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q $likeExpressions = []; foreach ($values as $val) { $parameterName = $queryNameGenerator->generateParameterName($property); - $likeExpressions[] = 'LOWER('.$field.') LIKE LOWER(:'.$parameterName.') ESCAPE \'\\\''; + $likeExpressions[] = $this->caseSensitive + ? $field.' LIKE :'.$parameterName.' ESCAPE \'\\\'' + : 'LOWER('.$field.') LIKE LOWER(:'.$parameterName.') ESCAPE \'\\\''; + $queryBuilder->setParameter($parameterName, $this->formatLikeValue($val)); } diff --git a/tests/Fixtures/TestBundle/Document/Chicken.php b/tests/Fixtures/TestBundle/Document/Chicken.php index 4847072ea8..2eba3607b4 100644 --- a/tests/Fixtures/TestBundle/Document/Chicken.php +++ b/tests/Fixtures/TestBundle/Document/Chicken.php @@ -32,10 +32,14 @@ 'name' => new QueryParameter(filter: new ExactFilter()), 'nameExactNoProperty' => new QueryParameter(filter: new ExactFilter()), 'namePartial' => new QueryParameter( - filter: new PartialSearchFilter(), + filter: new PartialSearchFilter(false), property: 'name', ), 'namePartialNoProperty' => new QueryParameter(filter: new PartialSearchFilter()), + '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 c85f7e1dff..81c0b45bab 100644 --- a/tests/Fixtures/TestBundle/Entity/Chicken.php +++ b/tests/Fixtures/TestBundle/Entity/Chicken.php @@ -36,6 +36,10 @@ property: 'name', ), 'namePartialNoProperty' => new QueryParameter(filter: new PartialSearchFilter()), + '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 5a83d156c2..2b497ff549 100644 --- a/tests/Functional/Parameters/PartialSearchFilterTest.php +++ b/tests/Functional/Parameters/PartialSearchFilterTest.php @@ -193,6 +193,43 @@ public function testPartialSearchFilterThrowsExceptionWhenPropertyIsMissing(): v ); } + #[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 7facbab6c1..80ac15a8b9 100644 --- a/tests/RecreateSchemaTrait.php +++ b/tests/RecreateSchemaTrait.php @@ -78,6 +78,11 @@ 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');