API Platform version(s) affected: 4.3.1
Description
I have a CompanyCategories entity with a name field, and in my front end, I need my users to consume this API.
Previously, I was using...
#[ApiFilter(SearchFilter::class, strategy: 'ipartial')]
public string $name,
and it works well, and I replaced this search with the parameters of the GetCollection resource.
new GetCollection(
paginationItemsPerPage: 100,
order: ['name' => 'ASC'],
parameters: [
'name' => new QueryParameter(filter: new AppPartialSearchFilter(caseSensitive: false)),
'isFavorite' => new QueryParameter(filter: new BooleanFilter()),
]
),
The problem
Before, when I searched for name = "edu", the "éducator" category would appear, but now it doesn't work anymore; I have to search for "edu".
The accent "é" of "éducateur' is keeped for the search
name=edu&itemsPerPage=5
How to reproduce
The problem is that now the sensitivity of the case respects accents.
Possible Solution
I came up with a solution with Claude that works but isn't exactly pretty while we wait for a fix.
<?php
declare(strict_types=1);
namespace App\Infrastructure\ApiPlatform\Filter;
use ApiPlatform\Doctrine\Common\Filter\OpenApiFilterTrait;
use ApiPlatform\Doctrine\Orm\Filter\FilterInterface;
use ApiPlatform\Doctrine\Orm\NestedPropertyHelperTrait;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait;
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\OpenApiParameterFilterInterface;
use ApiPlatform\Metadata\Operation;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\AST\Node;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\TokenType;
use Doctrine\ORM\QueryBuilder;
/**
* Partial search filter with accent and case insensitivity support (PostgreSQL unaccent).
*/
final class AppPartialSearchFilter implements FilterInterface, OpenApiParameterFilterInterface
{
use BackwardCompatibleFilterDescriptionTrait;
use NestedPropertyHelperTrait;
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'];
if (null === $parameter->getProperty()) {
throw new InvalidArgumentException(sprintf('The filter parameter with key "%s" must specify a property. Please provide the property explicitly.', $parameter->getKey()));
}
$queryBuilder->getEntityManager()
->getConfiguration()
->addCustomStringFunction('unaccent', UnaccentFunction::class);
$property = $parameter->getProperty();
$alias = $queryBuilder->getRootAliases()[0];
[$alias, $property] = $this->addNestedParameterJoins($property, $alias, $queryBuilder, $queryNameGenerator, $parameter);
$aliasedField = $alias . '.' . $property;
$values = $parameter->getValue();
$normalize = $this->createNormalize($this->caseSensitive);
if (!is_iterable($values)) {
$parameterName = ':' . $queryNameGenerator->generateParameterName($property);
$queryBuilder->setParameter($parameterName, $this->normalizeValue($values, $this->caseSensitive));
$queryBuilder->{$context['whereClause'] ?? 'andWhere'}(
$queryBuilder->expr()->like(
$normalize($aliasedField),
(string) $queryBuilder->expr()->concat("'%'", $parameterName, "'%'")
)
);
return;
}
$ors = [];
foreach ($values as $val) {
$parameterName = ':' . $queryNameGenerator->generateParameterName($property);
$queryBuilder->setParameter($parameterName, $this->normalizeValue($val, $this->caseSensitive));
$ors[] = $queryBuilder->expr()->like(
$normalize($aliasedField),
(string) $queryBuilder->expr()->concat("'%'", $parameterName, "'%'")
);
}
$queryBuilder->{$context['whereClause'] ?? 'andWhere'}(
$queryBuilder->expr()->orX(...$ors)
);
}
private function createNormalize(bool $caseSensitive): \Closure
{
return static function (string $expr) use ($caseSensitive): string {
$expr = sprintf('unaccent(%s)', $expr);
if (!$caseSensitive) {
$expr = sprintf('LOWER(%s)', $expr);
}
return $expr;
};
}
private function normalizeValue(string $value, bool $caseSensitive): string
{
if (!$caseSensitive) {
return strtolower($value);
}
return $value;
}
}
/**
* Doctrine DQL function for PostgreSQL unaccent().
* Registered dynamically by AppPartialSearchFilter — no doctrine.yaml config needed.
*/
final class UnaccentFunction extends FunctionNode
{
private Node $value;
public function parse(Parser $parser): void
{
$parser->match(TokenType::T_IDENTIFIER);
$parser->match(TokenType::T_OPEN_PARENTHESIS);
$this->value = $parser->StringPrimary();
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
}
public function getSql(SqlWalker $sqlWalker): string
{
return 'unaccent(' . $sqlWalker->walkStringPrimary($this->value) . ')';
}
}
Otherwise, thank you for all your work.
API Platform version(s) affected: 4.3.1
Description
I have a CompanyCategories entity with a name field, and in my front end, I need my users to consume this API.
Previously, I was using...
and it works well, and I replaced this search with the parameters of the GetCollection resource.
The problem
Before, when I searched for name = "edu", the "éducator" category would appear, but now it doesn't work anymore; I have to search for "edu".
The accent "é" of "éducateur' is keeped for the search
name=edu&itemsPerPage=5
How to reproduce
The problem is that now the sensitivity of the case respects accents.
Possible Solution
I came up with a solution with Claude that works but isn't exactly pretty while we wait for a fix.
Otherwise, thank you for all your work.