Archives Wild Code School
Symfony

26 - Symfony : Requêtes personnalisées

Quête 29 sur 35 · 2 images archivées

Objectifs

  • ✅ Créer une requête personnalisée
  • ✅ Construire une requête en utilisant le QueryBuilder ou le DQL
title-book
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. 1

    Création du formulaire

    Utilise la commande make:form pour créer un nouveau formulaire que tu nommeras SearchProgramType. 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 type SearchType.

    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. 2

    Le Contrôleur

    Dans ProgramController, mets à jour la méthode index() 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. 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)

image

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.

php
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. 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 p pour Program, mais tu pourrais mettre ce que tu veux.

    Info

    Où est le SELECT ? Il n'est pas nécessaire d'ajouter une méthode select() car par défaut, étant dans le ProgramRepository, la requête SQL construite fera automatiquement un SELECT sur la table des programmes. Il est cependant possible d'utiliser cette méthode select() pour des utilisations plus avancées.

  2. 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 (donc p.title)

    Attention

    Trè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é $title de Program et non directement au champ title de la table program. 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é $createdAt fera référence à une champ created_at, mais c'est Doctrine qui se chargera de faire le lien).

  3. 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 de bindValue() que tu utilisais avec PDO).

    Important

    Rappel 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. 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. 5

    getQuery()

    Cette méthode construit la requête SQL en fonction des méthodes utilisées juste avant.

  6. 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'objets Program.

    Info

    Cependant, 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
php
// ProgramController
if ($form->isSubmitted() && $form->isValid()) {
    $search = $form->getData()['search'];
    $programs = $programRepository->findLikeName($search);
} else {
    $programs = $programRepository->findAll();
}

↗
Doctrine QueryBuilder
doctrine-project.org

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 :

php
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 !

↗
Doctrine DQL
doctrine-project.org

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