• 30 heures
  • Facile

Ce cours est visible gratuitement en ligne.

Ce cours est en vidéo.

Ce cours existe en livre papier.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 04/09/2017

Récupérer ses entités avec Doctrine2

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

L'une des principales fonctions de la couche Modèle dans une application MVC, c'est la récupération des données. Récupérer des données n'est pas toujours évident, surtout lorsqu'on veut récupérer seulement certaines données, les classer selon des critères, etc. Tout cela se fait grâce aux repositories, que nous étudions dans ce chapitre. Bonne lecture !

Le rôle des repositories

On s'est déjà rapidement servi de quelques repositories, donc vous devriez sentir leur utilité, mais il est temps de théoriser un peu.

Définition

Un repository centralise tout ce qui touche à la récupération de vos entités. Concrètement, cela veut dire que vous ne devez pas faire la moindre requête SQL ailleurs que dans un repository, c'est la règle. On va donc y construire des méthodes pour récupérer une entité par son id, pour récupérer une liste d'entités suivant un critère spécifique, etc. Bref, à chaque fois que vous devez récupérer des entités dans votre base de données, vous utiliserez le repository de l'entité correspondante.

Rappelez-vous, il existe un repository par entité. Cela permet de bien organiser son code. Bien sûr, cela n'empêche pas qu'un repository utilise plusieurs entités, dans le cas d'une jointure par exemple.

Les repositories ne fonctionnent pas par magie, ils utilisent en réalité directement l'EntityManager pour faire leur travail. Vous le verrez, parfois nous ferons directement appel à l'EntityManager depuis des méthodes du repository.

Les méthodes de récupération des entités

Depuis un repository, il existe deux moyens de récupérer les entités : en utilisant du DQL et en utilisant le QueryBuilder.

Le Doctrine Query Language (DQL)

Le DQL n'est rien d'autre que du SQL adapté à la vision par objets que Doctrine utilise. Il s'agit donc de faire ce qu'on a l'habitude de faire, des requêtes textuelles comme celle-ci par exemple :

SELECT a FROM OCPlatformBundle:Advert a

Vous venez de voir votre première requête DQL. Retenez le principe : avec une requête qui n'est rien d'autre que du texte, on effectue le traitement voulu.

Le QueryBuilder

Le QueryBuilder est un moyen plus nouveau. Comme son nom l'indique, il sert à construire une requête, par étape. Si l'intérêt n'est pas évident au début, son utilisation se révèle vraiment pratique ! Voici la même requête que précédemment, mais en utilisant le QueryBuilder :

<?php
$QueryBuilder
  ->select('a')
  ->from('OCPlatformBundle:Advert', 'a')
;

Un des avantages est qu'il est possible de construire la requête en plusieurs fois. Ainsi, vous pouvez développer une méthode qui rajoute une condition à une requête, par exemple pour sélectionner tous les membres actifs (qui se sont connectés depuis moins d'un mois par exemple). Il suffit pour cela de passer le QueryBuilder à une méthode données, et cette méthode peut modifier la requête comme elle veut : ajouter un WHERE, ajouter une jointure, etc. C'est quelque chose qui est beaucoup plus compliqué avec une requête textuelle uniquement ! Pas de panique, on verra des exemples dans la suite du chapitre.

Les méthodes de récupération de base

Définition

Vos repositories héritent de la classeDoctrine\ORM\EntityRepository, qui propose déjà quelques méthodes très utiles pour récupérer des entités. Ce sont ces méthodes là que nous allons voir ici.

Les méthodes normales

Il existe quatre méthodes, que voici (tous les exemples sont effectués depuis un contrôleur).

find($id)

La méthodefind($id)récupère tout simplement l'entité correspondant à l'id$id. Dans le cas de notreAdvertRepository, elle retourne une instance d'Advert. Exemple :

<?php
$repository = $this
  ->getDoctrine()
  ->getManager()
  ->getRepository('OCPlatformBundle:Advert')
;

$advert = $repository->find(5);
// $advert est une instance de OC\PlatformBundle\Entity\Advert
// Correspondant à l'id 5
findAll()

La méthodefindAll()retourne toutes les entités contenue dans la base de données. Le format du retour est un simpleArray, que vous pouvez parcourir (avec unforeachpar exemple) pour utiliser les objets qu'il contient. Exemple :

<?php
$repository = $this
  ->getDoctrine()
  ->getManager()
  ->getRepository('OCPlatformBundle:Advert')
;

$listAdverts = $repository->findAll();

foreach ($listAdverts as $advert) {
  // $advert est une instance de Advert
  echo $advert->getContent();
}

Ou dans une vue Twig, si l'on a passé la variable$listAdverts au template :

<ul>
  {% for advert in listAdverts %}
    <li>{{ advert.content }}</li>
  {% endfor %}
</ul>
findBy()

La méthodefindBy()est un peu plus intéressante. CommefindAll(), elle permet de retourner une liste d'entités, sauf qu'elle est capable d'effectuer un filtre pour ne retourner que les entités correspondant à un ou plusieurs critère(s). Elle peut aussi trier les entités, et même n'en récupérer qu'un certain nombre (pour une pagination).

La syntaxe est la suivante :

<?php
$repository->findBy(
  array $criteria,
  array $orderBy = null,
  $limit  = null,
  $offset = null
);

Voici un exemple d'utilisation :

<?php

$listAdverts = $repository->findBy(
  array('author' => 'Alexandre'), // Critere
  array('date' => 'desc'),        // Tri
  5,                              // Limite
  0                               // Offset
);

foreach ($listAdverts as $advert) {
  // $advert est une instance de Advert
 echo $advert->getContent();
}

Cet exemple va récupérer toutes les entités ayant comme auteur « Alexandre » en les classant par date décroissante et en en sélectionnant cinq(5)à partir du début(0). Elle retourne unArrayégalement. Vous pouvez mettre plusieurs entrées dans le tableau des critères, afin d'appliquer plusieurs filtres.

findOneBy()

La méthodefindOneBy(array $criteria)fonctionne sur le même principe que la méthodefindBy(), sauf qu'elle ne retourne qu'une seule entité. Les argumentsorderBy,limitetoffsetn'existent donc pas. Exemple :

<?php

$advert = $repository->findOneBy(array('author' => 'Marine'));
// $advert est une instance de Advert

Ces méthodes permettent de couvrir pas mal de besoins. Mais pour aller plus loin encore, Doctrine nous offre deux autres méthodes magiques.

Les méthodes magiques

Vous connaissez le principe des méthodes magiques, comme__call()qui émule des méthodes. Ces méthodes émulées n'existent pas dans la classe, elle sont prises en charge par__call()qui va exécuter du code en fonction du nom de la méthode appelée.

Voici les deux méthodes gérées par__call()dans les repositories.

findByX($valeur)

Première méthode, en remplaçant « X » par le nom d'une propriété de votre entité. Dans notre cas, pour l'entitéAdvert, nous avons donc plusieurs méthodes :findByTitle(),findByDate(),findByAuthor(),findByContent(), etc.

Cette méthode fonctionne comme si vous utilisiez findBy() avec un seul critère, celui du nom de la méthode.

<?php

$listAdverts = $repository->findByAuthor('Alexandre');
// $listAdverts est un Array qui contient toutes les annonces
// écrites par Alexandre
findOneByX($valeur)

Deuxième méthode, en remplaçant « X » par le nom d'une propriété de votre entité. Dans notre cas, pour l'entitéAdvert, nous avons donc plusieurs méthodes :findOneByTitle(),findOneByDate(),findOneByAuthor(),findOneByContent(), etc.

Cette méthode fonctionne commefindOneBy(), sauf que vous ne pouvez mettre qu'un seul critère, celui du nom de la méthode.

<?php

$advert = $repository->findOneByTitle('Recherche développeur.');
// $advert est une instance d'Advert dont le titre
// est "Recherche développeur." ou null si elle n'existe pas.

Toutes ces méthodes permettent de récupérer vos entités dans la plupart des cas. Simplement, elles montrent rapidement leurs limites lorsqu'on doit faire des jointures, ou effectuer des conditions plus complexes. Pour cela — et cela nous arrivera très souvent — il faudra faire nos propres méthodes de récupération.

Les méthodes de récupération personnelles

La théorie

Pour effectuer nos propres méthodes, il faut bien comprendre comment fonctionne Doctrine2 pour effectuer ses requêtes. Il faut notamment distinguer trois types d'objets qui vont nous servir, et qu'il ne faut pas confondre : le QueryBuilder, la Query et les résultats.

Le QueryBuilder

On l'a déjà vu rapidement, le QueryBuilder permet de construire une Query, mais il n'est pas une Query !

Pour récupérer un QueryBuilder, on peut utiliser simplement l'EntityManager. En effet, il dispose d'une méthodecreateQueryBuilder()qui nous retournera une instance de QueryBuilder. L'EntityManager est accessible depuis un repository en utilisant l'attribut_em, soit$this->_em. Le code complet pour récupérer un QueryBuilder neuf depuis une méthode d'un repository est donc$this->_em->createQueryBuilder().

Cependant, cette méthode nous retourne un QueryBuilder vide, c'est-à-dire sans rien de prédéfini. C'est dommage, car lorsqu'on récupère un QueryBuilder depuis un repository, c'est que l'on veut faire une requête sur l'entité gérée par ce repository. Donc si l'on pouvait définir la partieSELECT advert FROM OCPlatformBundle:Advertsans trop d'effort, cela serait bien pratique, car ce qui est intéressant, c'est le reste de la requête. Heureusement, le repository contient également une méthodecreateQueryBuilder($alias)qui utilise la méthode de l'EntityManager, mais en définissant pour nous le SELECT et le FROM. Vous pouvez jeter un œil à cette méthodecreateQueryBuilder() pour comprendre.

L'alias en argument de la méthode est le raccourci que l'on donne à l'entité du repository. On utilise souvent la première lettre du nom de l'entité, dans notre exemple de l'annonce cela serait donc un « a ».

Beaucoup de théorie, passons donc à la pratique ! Pour bien comprendre la différence QueryBuilder / Query, ainsi que la récupération du QueryBuilder, rien de mieux qu'un exemple. Nous allons recréer la méthodefindAll()dans notre repositoryAdvert :

<?php
// src/OC/PlatformBundle/Entity/AdvertRepository.php

namespace OC\PlatformBundle\Entity;

use Doctrine\ORM\EntityRepository;

class AdvertRepository extends EntityRepository
{
  public function myFindAll()
  {
    // Méthode 1 : en passant par l'EntityManager
    $queryBuilder = $this->_em->createQueryBuilder()
      ->select('a')
      ->from($this->_entityName, 'a')
    ;
    // Dans un repository, $this->_entityName est le namespace de l'entité gérée
    // Ici, il vaut donc OC\PlatformBundle\Entity\Advert

    // Méthode 2 : en passant par le raccourci (je recommande)
    $queryBuilder = $this->createQueryBuilder('a');

    // On n'ajoute pas de critère ou tri particulier, la construction
    // de notre requête est finie

    // On récupère la Query à partir du QueryBuilder
    $query = $queryBuilder->getQuery();

    // On récupère les résultats à partir de la Query
    $results = $query->getResult();

    // On retourne ces résultats
    return $results;
  }
}

Cette méthodemyFindAll()retourne exactement le même résultat qu'unfindAll(), c'est-à-dire un tableau de toutes les entitésAdvert dans notre base de données.

Les méthodes 1 et 2 pour récupérer le QueryBuilder sont strictement équivalentes, c'est juste pour illustrer ce que j'ai dit précédemment.

Vous pouvez le voir, faire une simple requête est très facile. Pour mieux le visualiser, je vous propose la même méthode sans les commentaires et en raccourci :

<?php
public function myFindAll()
{
  return $this
    ->createQueryBuilder('a')
    ->getQuery()
    ->getResult()
  ;
}

Simplissime, non ? 

Et bien sûr, pour récupérer les résultats depuis un contrôleur il faut faire comme avec n'importe quelle autre méthode du repository, comme ceci :

<?php
// Depuis un contrôleur

public function testAction()
{
  $repository = $this
    ->getDoctrine()
    ->getManager()
    ->getRepository('OCPlatformBundle:Advert')
  ;
  
  $listAdverts = $repository->myFindAll();

  // ...
}

Bon pour l'instant c'est très simple car on a juste récupéré le QueryBuilder avec ses paramètres par défaut, mais on n'a pas encore joué avec lui.

Le QueryBuilder dispose de plusieurs méthodes afin de construire notre requête. Il y a une ou plusieurs méthodes par partie de requête : le WHERE, le ORDER BY, le FROM, etc. Ces méthodes n'ont rien de compliqué, voyez-le dans les exemples suivants.

Commençons par recréer une méthode équivalente aufind($id)de base, pour nous permettre de manipuler lewhere()et lesetParameter().

<?php
// Dans un repository

public function myFindOne($id)
{
  $qb = $this->createQueryBuilder('a');

  $qb
    ->where('a.id = :id')
    ->setParameter('id', $id)
  ;

  return $qb
    ->getQuery()
    ->getResult()
  ;
}

Vous connaissez le principe des paramètres, qui est le même qu'avec PDO. On définit un paramètre dans la requête avec:nom_du_parametre, puis on attribue une valeur à ce paramètre avec la méthodesetParameter('nom_du_parametre', $valeur).

Voici un autre exemple pour utiliser leandWhere()ainsi que leorderBy(). Créons une méthode pour récupérer toutes les annonces écrites par un auteur avant une année donnée :

<?php
// Depuis un repository 

public function findByAuthorAndDate($author, $year)
{
  $qb = $this->createQueryBuilder('a');

  $qb->where('a.author = :author')
       ->setParameter('author', $author)
     ->andWhere('a.date < :year')
       ->setParameter('year', $year)
     ->orderBy('a.date', 'DESC')
  ;

  return $qb
    ->getQuery()
    ->getResult()
  ;
}

Maintenant, voyons un des avantages du QueryBuilder. Vous vous en souvenez, je vous avais parlé d'une méthode pour centraliser une condition par exemple. Voyons donc une application de ce principe, en considérant que la condition « annonces postées durant l'année en cours » est une condition dont on va se resservir souvent. Il faut donc en faire une méthode, que voici :

<?php
// src/OC/PlatformBundle/Entity/AdvertRepository.php

namespace OC\PlatformBundle\Entity;

use Doctrine\ORM\EntityRepository;
// N'oubliez pas ce use
use Doctrine\ORM\QueryBuilder;

class AdvertRepository extends EntityRepository
{
  public function whereCurrentYear(QueryBuilder $qb)
  {
    $qb
      ->andWhere('a.date BETWEEN :start AND :end')
      ->setParameter('start', new \Datetime(date('Y').'-01-01'))  // Date entre le 1er janvier de cette année
      ->setParameter('end',   new \Datetime(date('Y').'-12-31'))  // Et le 31 décembre de cette année
    ;
  }
}

Vous notez donc que cette méthode ne traite pas une Query, mais bien uniquement le QueryBuilder. C'est en cela que ce dernier est très pratique, car faire cette méthode sur une requête en texte simple est possible, mais très compliqué. Il aurait fallu voir si le WHERE était déjà présent dans la requête, si oui mettre un AND au bon endroit, etc. Bref, pas simple.

Pour utiliser cette méthode, voici la démarche :

<?php
// Depuis un repository

public function myFind()
{
  $qb = $this->createQueryBuilder('a');

  // On peut ajouter ce qu'on veut avant
  $qb
    ->where('a.author = :author')
    ->setParameter('author', 'Marine')
  ;

  // On applique notre condition sur le QueryBuilder
  $this->whereCurrentYear($qb);

  // On peut ajouter ce qu'on veut après
  $qb->orderBy('a.date', 'DESC');

  return $qb
    ->getQuery()
    ->getResult()
  ;
}

Voilà, vous pouvez dorénavant appliquer cette condition à n'importe laquelle de vos requêtes en construction.

Je ne vous ai pas listé toutes les méthodes du QueryBuilder, il en existe bien d'autres. Pour cela, vous devez absolument mettre la page suivante dans vos favoris : http://www.doctrine-project.org/docs/o [...] -builder.html. Ouvrez-la et gardez-la sous la main à chaque fois que vous voulez faire une requête à l'aide du QueryBuilder, c'est la référence !

La Query

Vous l'avez vu, la Query est l'objet à partir duquel on extrait les résultats. Il n'y a pas grand-chose à savoir sur cet objet en lui-même, car il ne permet pas grand-chose à part récupérer les résultats. Il sert en fait surtout à la gestion du cache des requêtes.

Mais détaillons tout de même les différentes façons d'extraire les résultats de la requête. Ces différentes manières sont toutes à maîtriser, car elles concernent chacune un type de requête.

getResult()

Exécute la requête et retourne un tableau contenant les résultats sous forme d'objets. Vous récupérez ainsi une liste des objets, sur lequels vous pouvez faire des opérations, des modifications, etc.

Même si la requête ne retourne qu'un seul résultat, cette méthode retourne un tableau.

<?php
$listAdverts = $qb->getQuery()->getResult();

foreach ($listAdverts as $advert) {
  // $advert est une instance d'Advert dans notre exemple
  $advert->getContent();
}
getArrayResult()

Exécute la requête et retourne un tableau contenant les résultats sous forme de tableaux. Comme avecgetResult(), vous récupérez un tableau même s'il n'y a qu'un seul résultat. Mais dans ce tableau, vous n'avez pas vos objets d'origine, vous avez des simples tableaux. Cette méthode est utilisée lorsque vous ne voulez que lire vos résultats, sans y apporter de modification. Elle est dans ce cas plus rapide que son homologuegetResult().

<?php
$listAdverts = $qb->getQuery()->getArrayResult();

foreach ($listAdverts as $advert) {
  // $advert est un tableau
  // Faire $advert->getContent() est impossible. Vous devez faire :
  $advert['content'];
}

Heureusement, Twig est intelligent :{{ advert.content }}exécute$advert->getContent()si$advert est un objet, et exécute$advert['content']sinon. Du point de vue de Twig, vous pouvez utilisergetResult()ougetArrayResult()indifféremment.

Par contre attention, cela veut dire que si vous faites une modification à votre tableau, par exemple $advert['content'] = 'Nouveau contenu', elle ne sera pas enregistrée dans la base de données lors du prochainflush !

getScalarResult()

Exécute la requête et retourne un tableau contenant les résultats sous forme de valeurs. Comme avecgetResult(), vous récupérez un tableau même s'il n'y a qu'un seul résultat.

Mais dans ce tableau, un résultat est une valeur, non un tableau de valeurs (getArrayResult) ou un objet de valeurs (getResult). Cette méthode est donc utilisée lorsque vous ne sélectionnez qu'une seule valeur dans la requête, par exemple :SELECT COUNT(*) FROM …. Ici, la valeur est la valeur du COUNT.

<?php
$values = $qb->getQuery()->getScalarResult();

foreach ($values as $value) {
  // $value est la valeur de ce qui a été sélectionné : un nombre, un texte, etc.
  $value;

  // Faire $value->getAttribute() ou $value['attribute'] est impossible
}
getOneOrNullResult()

Exécute la requête et retourne un seul résultat, ounullsi pas de résultat. Cette méthode retourne donc une instance de l'entité (ounull) et non un tableau d'entités commegetResult().

Cette méthode déclenche une exceptionDoctrine\ORM\NonUniqueResultExceptionsi la requête retourne plus d'un seul résultat. Il faut donc l'utiliser si l'une de vos requêtes n'est pas censée retourner plus d'un résultat : déclencher une erreur plutôt que de laisser courir permet d'anticiper des futurs bugs !

<?php
$advert = $qb->getQuery()->getOneOrNullResult();

// $advert est une instance d'Advert dans notre exemple
// Ou null si la requête ne contient pas de résultat

// Et une exception a été déclenchée si plus d'un résultat
getSingleResult()

Exécute la requête et retourne un seul résultat. Cette méthode est exactement la même quegetOneOrNullResult(), sauf qu'elle déclenche une exceptionDoctrine\ORM\NoResultExceptionsi aucun résultat.

C'est une méthode très utilisée, car faire des requêtes qui ne retournent qu'un unique résultat est très fréquent. 

<?php
$advert = $qb->getQuery()->getSingleResult();

// $advert est une instance d'Advert dans notre exemple

// Une exception a été déclenchée si plus d'un résultat
// Une exception a été déclenchée si pas de résultat
getSingleScalarResult()

Exécute la requête et retourne une seule valeur, et déclenche des exceptions si pas de résultat ou plus d'un résultat.

Cette méthode est très utilisée également pour des requêtes du typeSELECT COUNT(*) FROM Advert, qui ne retournent qu'une seule ligne de résutlat, et une seule valeur dans cette ligne.

<?php
$value = $qb->getQuery()->getSingleScalarResult();

// $value est directement la valeur du COUNT dans la requête exemple

// Une exception a été déclenchée si plus d'un résultat
// Une exception a été déclenchée si pas de résultat
execute()

Exécute la requête. Cette méthode est utilisée principalement pour exécuter des requêtes qui ne retournent pas de résultats (desUPDATE,INSERT INTO, etc.) :

<?php
// Exécute un UPDATE par exemple :
$qb->getQuery()->execute();

Cependant, toutes les autres méthodes que nous venons de voir ne sont en fait que des raccourcis vers cette méthodeexecute(), en changeant juste le mode d'hydratation des résultats (objet, tableau, etc.).

<?php
// Voici deux méthodes strictement équivalentes :
$results = $query->getArrayResult();
// Et :
$results = $query->execute(array(), Query::HYDRATE_ARRAY);

// Le premier argument de execute() est un tableau de paramètres
// Vous pouvez aussi passer par la méthode setParameter(), au choix

// Le deuxième argument de execute() est ladite méthode d'hydratation

Pensez donc à bien choisir votre façon de récupérer les résultats à chacune de vos requêtes.

Utilisation du Doctrine Query Language (DQL)

Le DQL est une sorte de SQL adapté à l'ORM Doctrine2. Il permet de faire des requêtes un peu à l'ancienne, en écrivant une requête en chaîne de caractères (en opposition au QueryBuilder).

Pour écrire une requête en DQL, il faut donc oublier le QueryBuilder, on utilisera seulement l'objet Query. Et la méthode pour récupérer les résultats sera la même. Le DQL n'a rien de compliqué, et il est très bien documenté.

La théorie

Pour créer une requête en utilisant du DQL, il faut utiliser la méthodecreateQuery()de l'EntityManager :

<?php
// Depuis un repository
public function myFindAllDQL()
{
  $query = $this->_em->createQuery('SELECT a FROM OCPlatformBundle:Advert a');
  $results = $query->getResult();

  return $results;
}

Regardons de plus près la requête DQL en elle-même :

SELECT a FROM OCPlatformBundle:Advert a

Tout d'abord, vous voyez que l'on n'utilise pas de table. On a dit qu'on pensait objet et non plus base de données ! Il faut donc utiliser dans les FROM et les JOIN le nom des entités. Soit en utilisant le nom raccourci comme on l'a fait, soit le namespace complet de l'entité. De plus, il faut toujours donner un alias à l'entité, ici on a mis « a ». On met souvent la première lettre de l'entité, même si ce n'est absolument pas obligatoire.

Ensuite, vous imaginez bien qu'il ne faut pas sélectionner un à un les attributs de nos entités, cela n'aurait pas de sens. Une entitéAdvert avec le titre renseigné mais pas la date ? Ce n'est pas logique. C'est pourquoi on sélectionne simplement l'alias, ici « a », ce qui sélectionne en fait tous les attributs d'une annonce. L'équivalent d'une étoile (*) en SQL donc.

Faire des requêtes en DQL n'a donc rien de compliqué. Lorsque vous les faites, gardez bien sous la main la page de la documentation sur le DQL pour en connaître la syntaxe. En attendant, je peux vous montrer quelques exemples afin que vous ayez une idée globale du DQL.

Exemples

Pour faire une jointure :

SELECT a, u FROM Advert a JOIN a.user u WHERE u.age = 25

Pour utiliser une fonction SQL :

SELECT a FROM Advert a WHERE TRIM(a.author) = 'Alexandre'

Pour sélectionner seulement un attribut (attention les résultats seront donc sous forme de tableaux et non d'objets) :

SELECT a.title FROM Advert a WHERE a.id IN(1, 3, 5)

Et bien sûr vous pouvez également utiliser des paramètres :

<?php
public function myFindDQL($id)
{
  $query = $this->_em->createQuery('SELECT a FROM Advert a WHERE a.id = :id');
  $query->setParameter('id', $id);
  
  // Utilisation de getSingleResult car la requête ne doit retourner qu'un seul résultat
  return $query->getSingleResult();
}

Utiliser les jointures dans nos requêtes

Pourquoi utiliser les jointures ?

Je vous en ai déjà parlé dans le chapitre précédent sur les relations entre entités. Lorsque vous utilisez la syntaxe$entiteA->getEntiteB(), Doctrine exécute une requête afin de charger les entités B qui sont liées à l'entité A.

L'objectif est donc d'avoir la maîtrise sur quand charger juste l'entité A, et quand charger l'entité A avec ses entités B liées. Nous avons déjà vu le premier cas, par exemple un$repositoryA->find($id)ne récupère qu'une seule entité A sans récupérer les entités liées. Maintenant, voyons comment réaliser le deuxième cas, c'est-à-dire récupérer tout d'un coup avec une jointure, pour éviter une seconde requête par la suite.

Tout d'abord, rappelons le cas d'utilisation principal de ces jointures. C'est surtout lorsque vous bouclez sur une liste d'entités A (par exemple des annonces), et que dans cette boucle vous faites$entiteA->getEntiteB()(par exemple des candidatures). Avec une requête par itération dans la boucle, vous explosez votre nombre de requêtes sur une seule page ! C'est donc principalement pour éviter cela que nous allons faire des jointures.

Comment faire des jointures avec le QueryBuilder ?

Heureusement, c'est très simple ! Voici tout de suite un exemple :

<?php
// Depuis le repository d'Advert
public function getAdvertWithApplications()
{
  $qb = $this
    ->createQueryBuilder('a')
    ->leftJoin('a.applications', 'app')
    ->addSelect('app')
  ;

  return $qb
    ->getQuery()
    ->getResult()
  ;
}

L'idée est donc très simple :

  • D'abord on crée une jointure avec la méthodeleftJoin()(oujoin()pour faire l'équivalent d'unINNER JOIN). Le premier argument de la méthode est l'attribut de l'entité principale (celle qui est dans leFROMde la requête) sur lequel faire la jointure. Dans l'exemple, l'entitéAdvert possède un attributapplications. Le deuxième argument de la méthode est l'alias de l'entité jointe.

  • Puis on sélectionne également l'entité jointe, via unaddSelect(). En effet, unselect('app')tout court aurait écrasé leselect('a')déjà fait par lecreateQueryBuilder(), rappelez-vous.

Et pourquoi n'a-t-on pas précisé la condition « ON » du JOIN ?

C'est une bonne question. La réponse est très logique, pour cela réfléchissez plutôt à la question suivante : pourquoi est-ce qu'on rajoute unONhabituellement dans nos requêtes SQL ? C'est pour que MySQL (ou tout autre SGBDR) puisse savoir sur quelle condition faire la jointure. Or ici, on s'adresse à Doctrine et non directement à MySQL. Et bien entendu, Doctrine connaît déjà tout sur notre association, grâce aux annotations ! Il est donc inutile de lui préciser leON.

Bien sûr, vous pouvez toujours personnaliser la condition de jointure, en rajoutant vos conditions à la suite duONgénéré par Doctrine, grâce à la syntaxe duWITH:

<?php
$qb->join('a.applications', 'app', 'WITH', 'YEAR(app.date) > 2013')

Le troisième argument est le type de conditionWITH, et le quatrième argument est ladite condition.

« WITH » ? C'est quoi cette syntaxe pour faire une jointure ?

En SQL, la différence entre leONet leWITHest simple : unON définit la condition pour la jointure, alors qu'unWITH ajoute une condition pour la jointure. Attention, en DQL leONn'existe pas, seul leWITHest supporté. Ainsi, la syntaxe précédente avec leWITHserait équivalente à la syntaxe SQL suivante à base deON:

SELECT *
FROM Advert a
JOIN Application app ON (app.advert_id = a.id AND YEAR(app.date) > 2013)

Grâce auWITH, on n'a pas besoin de réécrire la condition par défaut de la jointure, leapp.advert_id = a.id.

Comment utiliser les jointures ?

Réponse : comme d'habitude ! Vous n'avez rien à modifier dans votre code (contrôleur, vue). Si vous utilisez une entité dont vous avez récupéré les entités liées avec une jointure, vous pouvez alors utiliser les getters joyeusement sans craindre de requête supplémentaire. Reprenons l'exemple de la méthodegetAdvertWithApplications()définie précédemment, on pourrait utiliser les résultats comme ceci :

<?php
// Depuis un contrôleur
public function listAction()
{
  $listAdverts = $this
    ->getDoctrine()
    ->getManager()
    ->getRepository('OCPlatformBundle:Advert')
    ->getAdvertWithApplications()
  ;

  foreach ($listAdverts as $advert) {
    // Ne déclenche pas de requête : les candidatures sont déjà chargées !
    // Vous pourriez faire une boucle dessus pour les afficher toutes
    $advert->getApplications();
  }

  // …
}

Voici donc comment vous devrez faire la plupart de vos requêtes. En effet, vous aurez souvent besoin d'utiliser des entités liées entre elles, et faire une ou plusieurs jointures s'impose très souvent. ;)

Application : les repositories de notre plateforme d'annonces

Plan d'attaque

Je vous propose quelques cas pratiques à implémenter dans nos repositories.

Dans un premier temps, nous allons ajouter une méthode dans l'AdvertRepository pour récupérer toutes les annonces qui correspondent à une liste de catégories. Par exemple, on veut toutes les annonces dans les catégories Développeur et Intégrateur. La définition de la méthode est donc :

<?php
public function getAdvertWithCategories(array $categoryNames);

Et on pourra l'utiliser comme ceci par exemple :

<?php
$repository->getAdvertWithCategories(array('Développeur', 'Intégrateur'));

Dans un deuxième temps, je vous propose de créer une méthode dans l'ApplicationRepository pour récupérer les X dernières candidatures avec leur annonce associée. La définition de la méthode doit être comme ceci :

<?php
public function getApplicationsWithAdvert($limit);

Le paramètre$limit étant le nombre de candidature à retourner.

À vous de jouer !

La correction

AdvertRepository.php:

<?php
// src/OC/PlatformBundle/Entity/AdvertRepository.php

namespace OC\PlatformBundle\Entity;

use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;

class AdvertRepository extends EntityRepository
{
  public function getAdvertWithCategories(array $categoryNames)
  {
    $qb = $this->createQueryBuilder('a');

    // On fait une jointure avec l'entité Category avec pour alias « c »
    $qb
      ->join('a.categories', 'c')
      ->addSelect('c')
    ;

    // Puis on filtre sur le nom des catégories à l'aide d'un IN
    $qb->where($qb->expr()->in('c.name', $categoryNames));
    // La syntaxe du IN et d'autres expressions se trouve dans la documentation Doctrine

    // Enfin, on retourne le résultat
    return $qb
      ->getQuery()
      ->getResult()
    ;
  }
}

Que faire avec ce que retourne cette fonction ?

Comme je l'ai dit précédemment, cette fonction va retourner un tableau d'Advert. Qu'est-ce que l'on veut en faire ? Les afficher. Donc la première chose à faire est de passer ce tableau à Twig. Ensuite, dans Twig, vous faites un simple{% for %}pour afficher ces annonces. Ce n'est vraiment pas compliqué à utiliser !

ApplicationRepository.php : 

<?php
// src/OC/PlatformBundle/Entity/AdvertRepository.php

namespace OC\PlatformBundle\Entity;

use Doctrine\ORM\EntityRepository;

class ApplicationRepository extends EntityRepository
{
  public function getApplicationsWithAdvert($limit)
  {
    $qb = $this->createQueryBuilder('a');

    // On fait une jointure avec l'entité Advert avec pour alias « adv »
    $qb
      ->join('a.advert', 'adv')
      ->addSelect('adv')
    ;

    // Puis on ne retourne que $limit résultats
    $qb->setMaxResults($limit);

    // Enfin, on retourne le résultat
    return $qb
      ->getQuery()
      ->getResult()
      ;
  }
}

Et voilà, vous avez tout le code. Je n'ai qu'une chose à vous dire à ce stade du cours : entraînez-vous ! Amusez-vous à faire des requêtes dans tous les sens dans tous les repositories. Jouez avec les relations entre les entités, créez-en d'autres. Bref, cela ne viendra pas tout seul, il va falloir travailler un peu de votre côté. ;) 

En résumé

  • Le rôle d'un repository est, à l'aide du langage DQL ou du constructeur de requêtes, de récupérer des entités selon des contraintes, des tris, etc.

  • Un repository dispose toujours de quelques méthodes de base, permettant de récupérer de façon très simple les entités.

  • Mais la plupart du temps, il faut créer des méthodes personnelles pour récupérer les entités exactement comme on le veut.

  • Il est indispensable de faire les bonnes jointures afin de limiter au maximum le nombre de requêtes SQL sur vos pages.

Exemple de certificat de réussite
Exemple de certificat de réussite