Blog de Norore
Geek en perdition dans un monde qui va trop vite

Symfony, Doctrine et unaccent pour PostGreSQL

11 Oct 2024 - Norore

Comme certains le savent peut-être, je développe et maintien, entre autres, une application maison en Symfony sur mon lieu de travail. Sur cette application, mes collègues gèrent la scolarité de nos étudiants et un besoin, déjà exprimé et répondu sur la première version de cette application, a de nouveau été soulevé : la recherche sur le nom ou le prénom sans tenir compte de l’accent.

L’application est basée sur une base de données en PostGreSQL. Une simple requête avec une fonction interne au SGBD permet de répondre à ce besoin. Le problème est que cette fonction, unaccent, ne semble pas exister dans Symfony ni dans Doctrine, l’ORM recommandé par le framework. Ce qui peut vite devenir un point bloquant notamment pour nos étudiants d’Europe de l’Est avec des lettres très spécifiques.

Fort heureusement, le projet Doctrine nous permet de créer des fonctions DQL personnalisées et fournit également quelques exemples pour s’en sortir : https://www.doctrine-project.org/projects/doctrine-orm/en/current/cookbook/dql-user-defined-functions.html

À force de farfouiller à droite à gauche, j’avais fini, il y a quelques années, par trouver un script sur GitHub, probablement celui-ci, aujourd’hui déprécié. Voici donc la mise à jour que j’ai apportée.

Depuis la racine de votre projet Symfony, si ce n’est déjà fait, créez un répertoire DQL/ dans src/ et créez le script UnaccentString.php :

mkdir -p src/DQL
touch src/DQL/UnaccentString.php

Ouvrez le fichier avec votre éditeur et ajoutez-y ces lignes :

<?php
namespace App\DQL;

use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Query\TokenType;

class UnaccentString extends FunctionNode
{
    private mixed $string;

    public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker): string
    {
        return 'UNACCENT('.$this->string->dispatch($sqlWalker).")";
    }

    /**
     * @throws QueryException
     */
    public function parse(\Doctrine\ORM\Query\Parser $parser): void
    {
        $parser->match(TokenType::T_IDENTIFIER);
        $parser->match(TokenType::T_OPEN_PARENTHESIS);

        $this->string = $parser->StringPrimary();

        $parser->match(TokenType::T_CLOSE_PARENTHESIS);
    }

}

C’est cette classe qui va vous permettre de dire à Doctrine comment utiliser la fonction unaccent de PostGreSQL.

Pour définir et appeler la fonction unaccent, ou tartampion si vous voulez la nommer ainsi, vous devez l’activer au niveau de Symfony en éditant le fichier config/packages/doctrine.yaml :

doctrine:
    [..]
    orm:
        [..]
        dql:
            string_functions:
                unaccent: App\DQL\UnaccentString

Une fois fait, vous devriez pouvoir l’appeler naturellement dans votre Repository, comme dans cet exemple issu de ma classe IndividuRepository :

<?php

namespace App\Repository;

use App\Entity\Individus;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Doctrine\Persistence\ManagerRegistry;

/**
 * @extends ServiceEntityRepository<Individus>
 *
 * @method Individus|null find($id, $lockMode = null, $lockVersion = null)
 * @method Individus|null findOneBy(array $criteria, array $orderBy = null)
 * @method Individus[]    findAll()
 * @method Individus[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
class IndividuRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Individus::class);
    }

    public function getIndividuPaginator(int $page, string $column, string $order, int $max, string $where): Paginator
    {
        $query = $this->createQueryBuilder('i')
            ->orderBy('i.' . $column, $order);
        if ($where !== "") {
            $query->where('lower(unaccent(i.nom)) LIKE lower(unaccent(:where))')
                ->orWhere('lower(unaccent(i.prenom)) LIKE lower(unaccent(:where))')
                ->setParameter('where', "%" . $where . "%");
        }
        $query->setMaxResults($max)
            ->setFirstResult($page * $max)
            ->getQuery();

        return new Paginator($query);
    }
}

Pour ma part, après déploiement, cette fonctionnalité est de nouveau opérationnelle et je vais pouvoir passer un bon week-end avec la satisfaction d’avoir aidé mes collègues et nos têtes blondes.