26 - Symfony : Requêtes personnalisées
Objectifs
- ✅ Créer une requête personnalisée
- ✅ Construire une requête en utilisant le QueryBuilder ou le DQL
Introduction
Comme tu l’as constaté, Doctrine facilite grandement la création et récupération de données. Symfony te permet ainsi de récupérer facilement des données. Mais plus ton application grossit, plus tu vas être amené à faire des requêtes complexes (jointures, aggrégations....). Les méthodes de bases (findAll(), findBy() vont alors trouver leur limites. Heureusement, Doctrine va te permettre d'effectuer des requêtes plus complexes et personnalisées grâce au QueryBuilder et au DQL.
Sommaire
Mise en place d'un formulaire de recherche
Tu vas utiliser les requêtes personnalisées dans le cadre d'un formulaire de recherche de séries, fonctionnalité fort intéressante si le nombre de séries sur ton site augmente. Un peu de mise en place est nécessaire.
- 1
Création du formulaire
Utilise la commande
make:formpour créer un nouveau formulaire que tu nommerasSearchProgramType. Attention, lorsque tu réponds aux questions, ne relie surtout pas le formulaire à l'entitéProgram(le formulaire ne sera relié à aucune entité, car il n'a pas pour but d'hydrater l'entitéProgram, juste de retourner une liste de résultats). Ton formulaire n'aura qu'un seul champ, que tu appelleras 'search' et qui sera de typeSearchType.Tu devrais pouvoir faire cela tout seul, mais si besoin, le code est ici ⬇️ .
Voir la solution
php<?php namespace App\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\SearchType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; class SearchProgramType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('search', SearchType::class) ; } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults([ // Configure your form options here ]); } } - 2
Le Contrôleur
Dans
ProgramController, mets à jour la méthodeindex()afin qu'elle puisse gérer ce nouveau formulaire. Puisque ce formulaire n'est relié à aucune entité, tu n'es pas obligé de passer un second paramètre à la méthode$this->createForm()et dans ce cas, tu peux récupérer les données du formulaire sous forme d'un tableau en utilisant la méthode$form->getData(). Dans un premier temps, ta page devra continuer à te renvoyer toutes les séries si le formulaire n'est pas soumis, et seulement la série recherchée si le nom exact est saisi dans le formulaire.Là encore, essaie d'avancer tout seul, mais si besoin, le code est ici ⬇️ .
Voir la solution
php// ProgramController public function index(Request $request, ProgramRepository $programRepository): Response { $form = $this->createForm(SearchProgramType::class); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $search = $form->getData()['search']; $programs = $programRepository->findBy(['title' => $search]); } else { $programs = $programRepository->findAll(); } return $this->render('program/index.html.twig', [ 'programs' => $programs, 'form' => $form, ]); } - 3
La vue
N'oublie pas d'ajouter la vue du formulaire dans program/index.html.twig.
Voir la solution
Attention : seule la partie du formulaire est donnée, place le formulaire de recherche sous le
<h1>.html<h1>Toutes les séries</h1> {{ form_start(form) }} {{ form_widget(form) }} <button>Search</button> {{ form_end(form) }}
Création d'une requête personnalisée
Ton formulaire te permet de rechercher une série. Mais pour le moment, tu dois taper le nom exact de la série, ce qui n'est pas très pratique. Ce serait tellement mieux de pouvoir faire une recherche partielle, par exemple "Walk" pour trouver "Walking Dead" mais aussi "Walker Texas Ranger" (même si une série avec Chuck Norris au générique ne devrait pas avoir besoin d'un formulaire de recherche pour figurer dans les résultats :D)

Pour effectuer cette requête partielle en SQL, tu dois utiliser le mot clé LIKE qui s'accompagne de % (jokers).
Or, La méthode findBy() utilisée actuellement dans le contrôleur ne permet pas d'utiliser un LIKE. Tu vas devoir créer ta première requête personnalisée.
Pour ce faire, rends toi dans le fichier App\Repository\ProgramRepository (il y a un fichier Repository généré automatiquement pour chaque entité si tu as utilisé la commande make:entity). Tu peux supprimer le code d'exemple commenté généré par défaut.
Crée une nouvelle méthode publique findLikeName(string $name).
Puis tu vas utiliser le QueryBuilder, objet proposé par Doctrine, qui va te permettre de construire une requête via un ensemble de méthodes, sans avoir à taper directement du SQL, et tout en faisant le lien avec nos entités.
public function findLikeName(string $name)
{
$queryBuilder = $this->createQueryBuilder('p')
->where('p.title LIKE :name')
->setParameter('name', '%' . $name . '%')
->orderBy('p.title', 'ASC')
->getQuery();
return $queryBuilder->getResult();
}
Voici quelques explications sur tout ce code.
- 1
createQueryBuilder('p')
C'est ici que tu créés ton "QueryBuilder". En paramètre, tu dois obligatoirement mettre un alias (par convention, c'est souvent la première lettre de l'entité concernée, ici
ppourProgram, mais tu pourrais mettre ce que tu veux.InfoOù est le SELECT ? Il n'est pas nécessaire d'ajouter une méthode
select()car par défaut, étant dans leProgramRepository, la requête SQL construite fera automatiquement un SELECT sur la table des programmes. Il est cependant possible d'utiliser cette méthodeselect()pour des utilisations plus avancées. - 2
where('p.title LIKE :name')
La méthode
where()permet d'exécuter une clause WHERE de SQL, c'est donc ici que le LIKE se place. Tu es obligé de préfixer ton nom de champ par l'alias choisi précédemment (doncp.title)AttentionTrès important Note qu'ici, tu construis une requête pour Doctrine (et non une requête SQL). Cela signifie que lorsque tu écris
p.title, cela réfère à la propriété$titledeProgramet non directement au champtitlede la tableprogram. C'est crucial de comprendre cela (une erreur est vite arrivée) car le champ en base de données peut s'écrire différement de la propriété (par exemple une propriété$createdAtfera référence à une champcreated_at, mais c'est Doctrine qui se chargera de faire le lien). - 3
Requête préparée
L'information provenant de l'utilisateur, tu utiliseras un placeholder pour effectuer une requête préparée et éviter les injections SQL. Pour spécifier la valeur de ce placeholder, c'est la méthode
setParameter()qui est utilisée (c'est l'équivalent debindValue()que tu utilisais avec PDO).ImportantRappel sécurité Le QueryBuilder te donne la possibilité de faire une requête préparée mais ce n'est ni obligatoire, ni automatique. Il en est de TA responsabilité de développeur de penser à effectuer cette requête préparée et donc protéger ton application des injections SQL.
- 4
Méthodes supplémentaires
Tu peux ajouter d'autres méthodes (il en existe beaucoup) pour construire cette requête, comme
orderBy(),setMaxResults(),groupBy(),join(), etc. Réfère-toi à la documentation de Doctrine pour avoir une liste exhaustive des méthodes disponibles et de leur utilisation. - 5
getQuery()
Cette méthode construit la requête SQL en fonction des méthodes utilisées juste avant.
- 6
getResult()
Cette méthode exécute la requête SQL et retourne les résultats. Par défaut, puisque l'on se trouve dans le
ProgramRepository, cette méthode va renvoyer un tableau d'objetsProgram.InfoCependant, il est possible de renvoyer les résultats sous d'autres formats via des variantes de cette méthode, comme
getArrayResult(),getOneOrNullResult(), etc. Là encore, réfère-toi à la documentation si tu veux en savoir plus.
Il ne te reste plus qu'à modifier la méthode findBy() dans le ProgramController par findLikeName($search);
Voir la solution
// ProgramController
if ($form->isSubmitted() && $form->isValid()) {
$search = $form->getData()['search'];
$programs = $programRepository->findLikeName($search);
} else {
$programs = $programRepository->findAll();
}
L'alternative DQL
Le QueryBuilder, c’est bien, mais c’est assez éloigné de ce que nous avons l’habitude de faire en SQL. Et pour tirer le meilleur du SQL et de nos entités, Doctrine met à notre disposition le DQL pour Doctrine Query Language. Ce dernier permet d’écrire des requêtes très proches du SQL, tout en faisant le lien avec nos entités et leurs paramètres.
Par exemple, avoir la liste des séries avec une première saison sortie après 2010 :
public function findRecentPrograms()
{
$em = $this->getEntityManager();
$query = $em->createQuery('SELECT p, s FROM App\Entity\Program p
INNER JOIN p.seasons s
WHERE s.year>2010');
return $query->execute();
}
Ici également, il faut continuer de penser objet. Ainsi, pour le cas d'une jointure, tu ne vas pas utiliser le nom d'une table, mais le nom pleinement qualifié d'une autre classe entité ainsi que le nom des propriétés, sans se soucier des noms réels dans le schéma en base de données.
Le DQL et le QueryBuilder offrent exactement les mêmes possibilités. Généralement, on préférera utiliser le QueryBuilder, car il permet de construire de façon plus dynamique une requête (grâce à son approche objet), sans avoir à manipuler une chaîne de caractères. Cela facilite les choses lorsque la requête doit être construite avec des conditions par exemples. De plus, ces requêtes sont plus faciles à tester via des librairies dédiées. Le DQL offre une syntaxe plus proche du SQL natif ce qui peut faciliter les choses pour écrire des requêtes longues et complexes.
Dans tous les cas, note que les requêtes écrites via le QueryBuilder seront au final transformées en DQL avant d'être exécutées !
Optimisation
Les requêtes personnalisées sont également très utiles pour optimiser les performances de certaines requêtes. Par exemple, boucler sur la liste des séries pour récupérer leur acteurs, générera autant de requêtes que de séries en base de données, ce qui peut être limitant si la base grossit. Créer une requête personnalisée effectuant une jointure entre les entités Actor et Program permet de récupérer les mêmes informations via une unique requête, améliorant ainsi grandement les performances globales. Garde donc toujours cela en tête.
D'ailleurs tu peux facilement voir le nombre de requêtes exécutées sur une page dans le Profiler de Symfony, qui t'aide à optimiser ton site. Dans cet exemple, 1001 requêtes sont effectuées sur la page, pour un temps de 82ms, une optimisation serait donc plus que nécessaire !

💪 Challenge
Filtres avancés
Maintenant que tu sais comment créer des requêtes personnalisées, tu vas améliorer ton formulaire de recherche. Fais en sorte que la recherche trouve non seulement des Program par leur titre, mais également par le nom de famille d'un acteur jouant dans celle-ci (tu pourras t'aider de join() et orWhere()).
Critères de validation
- La requête récupére des séries par leur titre ou le nom de famille d'un acteur
- La requête utilise le QueryBuilder
- Le code est disponible sur un repository GitHub, avec une branche correspondant à cette quête.
Ressources partagées sur cette quête
- video on custum requestshttps://symfonycasts.com/screencast/doctrine-queries/and-where-or-where#play
- video on joins in custom requestshttps://symfonycasts.com/screencast/doctrine-queries/joins#play