Aller au menu - Aller au contenu

Icône Récupérer ses entités avec Doctrine2

Avatar
Mise à jour : 02/05/2012
Difficulté : Intermédiaire Intermédiaire Durée d'étude : 3 heures Creative Commons BY-NC-SA
20 752 visites depuis 7 jours, dont 1 016 sur ce chapitre classé 17/786
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 ceci se fait grâce aux repository, que nous étudions dans ce chapitre. Bonne lecture !
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

Le rôle des Repository

On s'est déjà rapidement servi de quelques repository, 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 donc, 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é 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 types d'entité, dans le cas d'une jointure par exemple.

Les repository ne fonctionnent pas par magie, ils utilisent en réalité directement l'EntityManager pour faire leur travail. Vous le verrez, des fois 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 objet 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 :
Code : SQL
1
SELECT a FROM SdzBlogBundle:Article 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 :
Code : PHP
1
2
3
4
<?php
$QueryBuilder
  ->select('a')
  ->from('SdzBlogBundle:Article', '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). Comme cette condition risque de servir souvent, dans plusieurs requêtes, avant vous deviez la réécrire à chaque fois. Avec le QueryBuilder, vous pourrez faire appel à la même méthode, sans réécrire la condition. Pas de panique on verra des exemples dans la suite du chapitre !

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

Définition


Vos repository héritent de la classe Doctrine\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 depuis un contrôleur) :

Méthode Explications Exemple
find($id) La méthode find($id) récupère tout simplement l'entité correspondant à l'id $id. Dans le cas de notre ArticleRepository, elle retourne une instance d'Article. Code : PHP
1
2
3
4
5
6
7
<?php
$repository = $this->getDoctrine()
                   ->getEntityManager()
                   ->getRepository('SdzBlogBundle:Article');

$article_5 = $repository->find(5);
// $article 5 est une instance de Sdz\BlogBundle\Entity\Article
findAll() La méthode findAll() retourne toutes les entités. Elle ne retourne pas un Array mais un ArrayCollection, nous avons vu cette classe dans le chapitre sur les relations. Elle s'utilise comme un tableau et vous pouvez bien sûr la parcourir avec un foreach. Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php
$repository = $this->getDoctrine()
                   ->getEntityManager()
                   ->getRepository('SdzBlogBundle:Article');

$liste_articles = $repository->findAll();

foreach($liste_articles as $article)
{
    // $article est une instance de Article
    echo $article->getContenu();
}

Ou dans une vue Twig, si l'on a passé la variable $liste_articles au template :
Code : HTML
1
2
3
4
5
<ul>
  {% for article in liste_articles %}
    <li>{{ article.contenu }}</li>
  {% endfor %}
</ul>
<?php findBy(array $criteres, array $orderBy = null, $limite = null, $offset = null) La méthode findBy() est un peu plus intéressante. Comme findAll() elle permet de retourner une liste d'entité, sauf qu'elle est capable d'effectuer un filtre pour ne retourner que les entités correspondant à un critère. Elle peut aussi trier les entités, et même n'en récupérer qu'un certain nombre (pour une pagination). Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php
$repository = $this->getDoctrine()
                   ->getEntityManager()
                   ->getRepository('SdzBlogBundle:Article');

$liste_articles = $repository->findBy(array('auteur' => 'winzou'),
                                      array('date' => 'desc'),
                                      5,
                                      0);

foreach($liste_articles as $article)
{
    // $article est une instance de Article
    echo $article->getContenu();
}

Cet exemple va récupérer toutes les entités ayant comme auteur « winzou » en les classant par date décroissante et en en sélectionnant cinq (5) à partir du début (0). Elle retourne un ArrayCollection également.
Vous pouvez mettre plusieurs entrées dans le tableau des critères, afin d'appliquer plusieurs filtres.
findOneBy(array $criteres) La méthode findOneBy($criteres) fonctionne sur le même principe que la méthode findBy(), sauf qu'elle ne retourne qu'une seule entité. Les arguments orderBy, limite et offset n'existe donc pas. Code : PHP
1
2
3
4
5
6
7
<?php
$repository = $this->getDoctrine()
                   ->getEntityManager()
                   ->getRepository('SdzBlogBundle:Article');

$article = $repository->findOneBy(array('titre' => 'Mon dernier weekend'));
// $article est une instance de Article


Ces méthodes permettent de couvrir pas mal de besoin. 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 repository :

Méthode Explications Exemple
findByX($valeur) En remplaçant « X » par le nom d'une propriété de votre entité. Dans notre cas, pour l'entité Article, nous avons donc plusieurs méthodes : findByTitre(), findByDate(), findByAuteur(), findByContenu(), etc.

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

Attention la limite de cette méthode est que vous ne pouvez pas utiliser les arguments pour trier, ni pour mettre une limite.
Code : PHP
1
2
3
4
5
6
7
<?php
$repository = $this->getDoctrine()
                   ->getEntityManager()
                   ->getRepository('SdzBlogBundle:Article');

$liste_articles = $repository->findByAuteur('winzou');
// $liste_articles est un ArrayCollection qui contient tous les articles écrits par winzou
findOneByX($valeur) En remplaçant « X » par le nom d'une propriété de votre entité. Dans notre cas, pour l'entité Article, nous avons donc plusieurs méthodes : findOneByTitre(), findOneByDate(), findOneByAuteur(), findOneByContenu(), etc.

Cette méthode fonctionne comme findOneBy(), sauf que vous ne pouvez mettre qu'un seul critère, celui du nom de la méthode.
Code : PHP
1
2
3
4
5
6
7
<?php
$repository = $this->getDoctrine()
                   ->getEntityManager()
                   ->getRepository('SdzBlogBundle:Article');

$article = $repository->findOneByTitre('Mon dernier weekend');
// $article est une instance d'Article


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 3 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éthode createQueryBuilder() qui nous retournera une instance de QueryBuilder. L'EntityManager est accessible depuis un repository en utilisant l'attribut "_em" d'un repository, soit <?php $this->_em. Le code complet pour récupérer un QueryBuilder neuf depuis une méthode d'un repository est donc <?php $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 partie "SELECT article FROM SdzBlogBundle:Article" sans trop d'effort, ça serait bien pratique car ce qui est intéressant c'est le reste de la requête. Heureusement, le repository contient également une méthode createQueryBuilder($alias) qui utilise la méthode de l'EntityManager, mais en définissant pour nous le SELECT et le FROM. Vous pouvez jeter un oeil à cette méthode createQueryBuilder() 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'Article 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éthode findAll() dans notre repository Article :
Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
// src/Sdz/BlogBundle/Entity/ArticleRepository

namespace Sdz\BlogBundle\Entity;

use Doctrine\ORM\EntityRepository;

/**
 * ArticleRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
class ArticleRepository extends EntityRepository
{
	public function myFindAll()
	{
		$queryBuilder = $this->createQueryBuilder('a');
		
		// Méthode équivalente, mais plus longue :
		$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 Sdz\BlogBundle\Entity\Article
		
		// On a fini de construire notre requête.
		
		// On récupère la Query à partir du QueryBuilder
		$query = $queryBuilder->getQuery();
		
		// On récupère les résultats à partir de la Query
		$resultats = $query->getResult();
		
		// On retourne ces résultats
		return $resultats;
	}
}

Cette méthode myFindAll() retourne exactement le même résultat qu'un findAll(), c'est-à-dire un ArrayCollection de toutes les entités Article dans notre base de données. 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 raccourcie :
Code : PHP
1
2
3
4
5
6
7
<?php
public function myFindAll()
{
    return $this->createQueryBuilder('a')
                 ->getQuery()
                 ->getResult();
}

Simplissime non ? :p

Et bien sûr pour récupérer les résultats depuis un contrôleur, le code est le suivant :
Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php
public function testAction()
{
    $liste_articles = $this->getDoctrine()
                           ->getEntityManager()
                           ->getRepository('SdzBlogBundle:Article')
                           ->myFindAll();

    // Reste de la méthode du contrôleur.
}


Sauf que pour l'instant on a juste récupéré le QueryBuilder, on n'a pas encore joué avec lui. Il dispose de plusieurs méthodes afin de construire notre requête. Il y a une ou plusieurs méthode(s) par partie de requête : le WHERE, le ORDER BY, le FROM, etc. Elles n'ont rien de compliqué, voyez-le dans les exemples suivants.

Commençons par une méthode équivalente au find($id) de base, pour nous permettre de manipuler le where() et le setParameter().
Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<?php
// Dans un repository

public function myFindOne($id)
{
    // On passe par le QueryBuilder vide de l'EntityManager pour l'exemple
    $qb = $this->_em->createQueryBuilder();

    $qb->select('a')
       ->from('SdzBlogBundle:Article', 'a')
       ->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 attribut une valeur à ce paramètre avec la méthode setParameter('nom_du_parametre', $valeur).

Voici un autre exemple pour utiliser le andWhere() ainsi que le orderBy(). Créons une méthode pour récupérer tous les articles écrits par un auteur avant une année donnée :
Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?php
// Depuis un repository 

public function findByAuteurAndDate($auteur, $annee)
{
    // On utilise le QueryBuilder créé par le repository directement pour gagner du temps
    // Plus besoin de faire le select() ni le from() par la suite donc
    $qb = $this->createQueryBuilder('a');

    $qb->where('a.auteur = :auteur')
         ->setParameter('auteur', $auteur)
       ->andWhere('a.date < :annee')
         ->setParameter('annee', $annee)
       ->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 "articles postés 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 :
Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?php
// Depuis un repository

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

    return $qb;
}

Vous notez donc que cette méthode ne traite pas une Query, mais bien uniquement le QueryBuilder. C'est en ça qu'il est très pratique, car faire cette méthode sur une requête en texte simple est possible, mais 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 :
Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php
// Depuis un repository

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

    // On peut rajouter ce qu'on veut avant
    $qb->where('a.auteur = :auteur')
         ->setParameter('auteur', 'winzou');

    // On applique notre condition
    $qb = $this->whereCurrentYear($qb);

    // On peut rajouter 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. Un prochain chapitre est à venir sur ce cache de requêtes.

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

Méthode Explications Exemple
getResult() Exécute la requête et retourne un tableau contenant les résultats sous forme d'objet. 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.
Code : PHP
1
2
3
4
5
6
7
8
<?php
$entites = $qb->getQuery()->getResult();

foreach($entites as $entite)
{
    // $entite est une instance d'Article pour notre exemple
    $entite->getAttribut();
}
getArrayResult() Exécute la requête et retourne un tableau contenant les résultats sous forme de tableau.

Comme avec getResult(), 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 homologue getResult().
Code : PHP
1
2
3
4
5
6
7
8
9
<?php
$entites = $qb->getQuery()->getArrayResult();

foreach($entites as $entite)
{
    // $entite est un tableau
    // Faire $entite->getAttribut() est impossible. Vous devez faire :
    $entite['attribut'];
}

Heureusement Twig est intelligent : {{ entite.attribut }} exécute $entite->getAttribut() si $entite est un objet, et exécute $entite['attribut'] sinon. Du point de vue de Twig, vous pouvez utiliser getResult() ou getArrayResult() indifféremment.
getScalarResult() Exécute la requête et retourne un tableau contenant les résultats sous forme de valeur.

Comme avec getResult(), 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 valeur (getArrayResult) ou un objet de valeur (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.
Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php
$entites = $qb->getQuery()->getScalarResult();

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

    // Faire $valeur->getAttribut() ou $valeur['attribut'] est impossible.
}
getOneOrNullResult() Exécute la requête et retourne un seul résultat, ou null si pas de résultat. Cette méthode retourne donc une instance de l'entité (ou null) et non un tableau d'entités comme getResult().

Cette méthode déclenche une exception Doctrine\ORM\NonUniqueResultException si 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 !
Code : PHP
1
2
3
4
5
6
7
<?php
$entite = $qb->getQuery()->getOneOrNullResult();

// $entite est une instance d'Article 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 que getOneOrNullResult(), sauf qu'elle déclenche une exception Doctrine\ORM\NoResultException si aucun résultat.

C'est une méthode très utilisée, car faire des requêtes qui ne retourne qu'un unique résultat est très fréquent.
Code : PHP
1
2
3
4
5
6
7
<?php
$entite = $qb->getQuery()->getSingleResult();

// $entite est une instance d'Article 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 exception 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 type SELECT COUNT(*) FROM Article, qui ne retourne qu'une seule ligne de résutlat, et une seule valeur dans cette ligne.
Code : PHP
1
2
3
4
5
6
7
<?php
$valeur = $qb->getQuery()->getSingleScalarResult();

// $valeur 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 (des UPDATE, INSERT INTO, etc.).

Cependant, toutes les autres méthodes que nous venons de voir ne sont en fait que des raccourcis vers cette méthode execute(), en changeant juste le mode d'hydratation des résultats (objet, tableau, etc.).
Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php
// Exécute un UPDATE par exemple :
$qb->getQuery()->execute();

// Voici deux méthodes strictement équivalentes :
$resultats = $query->getArrayResult();
// Et :
$resultats = $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éthode createQuery() de l'EntityManager :
Code : PHP
1
2
3
4
5
6
7
8
9
<?php
// Depuis un repository
public function myFindAllDQL()
{
    $query = $this->_em->createQuery('SELECT a FROM SdzBlogBundle:Article a');
    $resultats = $query->getResult();

    return $resultats;
}

Regardons de plus près la requête DQL en elle-même :
Code : SQL
1
SELECT a FROM SdzBlogBundle:Article a

Tout d'abord, vous voyez que l'on 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é Article 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'un Article. L'équivalent d'une étoile (*) en SQL donc.

Sachez qu'il est tout de même possible de ne sélectionner qu'une partie d'un objet, en faisant a.titre par exemple. Mais vous ne recevez alors qu'un tableau contenant les attributs sélectionnés, et non un objet. Vous ne pouvez donc pas modifier/supprimer/etc l'objet, puisque c'est un tableau. Cela sert dans des requêtes particulières, mais la plupart du temps on sélectionnera bien tout l'objet.


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 connaitre la syntaxe. En attendant, je peux vous montrer quelques exemples afin que vous ayez une idée globale du DQL.

Pour tester rapidement vos requêtes DQL, sans avoir à les implémenter dans une méthode de votre repository, Doctrine2 nous simplifie la vie grâce à la commande doctrine:query:dql. Cela vous permet de faire quelques tests afin de construire ou de vérifier vos requêtes, à utiliser sans modération donc ! Je vous invite dès maintenant à exécuter la commande suivante :
Code : Console
php app/console doctrine:query:dql "SELECT a FROM SdzBlogBundle:Article a"


Exemples


Pour faire une jointure :
Code : SQL
1
SELECT a, u FROM Article a JOIN a.utilisateur u WHERE u.age = 25

Pour utiliser une fonction SQL :
Code : SQL
1
SELECT a FROM Article a WHERE TRIM(a.auteur) = 'winzou'

Pour sélectionner seulement un attribut (attention les résultats seront donc sous forme de tableaux et non d'objets) :
Code : SQL
1
SELECT a.titre FROM Article a WHERE a.id IN(1, 3, 5)

Et bien sûr vous pouvez également utiliser des paramètres :
Code : PHP
1
2
3
4
5
6
7
<?php
public function myFindDQL($id)
{
    $query = $this->_em->createQuery('SELECT a FROM Article a WHERE a.id = :id');
    $query->setParameter('id', $id);
    return $query->getSingleResult(); // Utilisation de getSingleResult car la requête ne doit retourner qu'un seul résultat
}

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 <?php $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 articles), et que dans cette boucle vous faites $entiteA->getEntiteB() (par exemple des commentaires). Avec 1 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 :
Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<?php
// Depuis le repository d'Article
public function getArticleAvecCommentaires()
{
    $qb = $this->createQueryBuilder('a')
               ->join('a.commentaires', 'c')
               ->addSelect('c');

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

L'idée est donc très simple :
  • D'abord on crée une jointure avec la méthode join() (ou leftJoin()). Le premier argument de la méthode est l'attribut de l'entité principale (celle qui est dans le FROM de la requête) sur lequel faire la jointure. Dans l'exemple, l'entité Article possède un attribut "commentaires". Le deuxième argument de la méthode est l'alias de l'entité jointe.
  • Puis on sélectionne également l'entité jointe, via un addSelect(). En effet un select() tout court aurait écrasé le select('a') déjà fait par le createQueryBuilder(), rappelez-vous.

Attention : On ne peut faire une jointure que si l'entité du FROM possède un attribut vers l'entité à joindre ! Cela veut dire que soit l'entité du FROM est l'entité propriétaire de la relation, soit la relation est bidirectionnelle.
Dans notre exemple, la relation entre Article et Commentaire est une ManyToOne avec Commentaire le côté Many, le côté propriétaire donc. Cela veut dire que pour pouvoir faire la jointure dans ce sens, la relation est bidirectionnelle, afin d'ajouter un attribut "commentaires" dans l'entité inverse Article.


Et pourquoi on n'a 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 un ON habituellement 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 connait déjà tout sur notre association, grâce aux annotations !

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

Code : PHP
1
2
<?php
$qb->join('a.commentaires', 'c', 'WITH', 'YEAR(c.date) > 2011')

Le 3e argument est le type de condition WITH, et le 4e argument est ladite condition.

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

En SQL, la différence entre le ON et le WITH est simple : un ON définit la condition pour la jointure, alors qu'un WITH ajoute une condition pour la jointure. Attention, en DQL le ON n'existe pas, seul le WITH est supporté. Ainsi, la syntaxe précédente avec le WITH serait équivalente à la syntaxe SQL suivante à base de ON :

Code : SQL
1
SELECT * FROM Article a JOIN Commentaire c ON c.article = a.id AND YEAR(c.date) > 2011

Grâce au WITH, on n'a pas besoin de réécrire la condition par défaut de la jointure, le "c.article = a.id".

Comment utiliser les jointures ?


Réponse : comme d'habitude ! Vous n'avez rien à modifier dans votre code. Si vous utilisez une entité dont vous avez récupéré les entités liées avec une jointure, vous pouvez alors utiliser les getter joyeusement sans craindre de requête supplémentaire. Reprenons l'exemple de la méthode getArticleAvecCommentaires() définie un peu plus haut, on pourrait utiliser les résultats comme ceci :

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php
// Depuis un contrôleur
public function listeAction()
{
    $liste_articles = $this->getDoctrine()
                           ->getEntityManager()
                           ->getRepository('SdzBlogBundle:Article')
                           ->getArticleAvecCommentaires();

    foreach($liste_articles as $article)
    {
        $article->getCommentaires(); // Ne déclenche pas de requête : les commentaires sont déjà chargés !
                                     // Vous pourriez faire une boucle dessus pour les afficher tous.
    }

    // ...
}


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 entités de notre blog

Plan d'attaque


On a déjà un peu traité l'entité Article au début de cette partie. J'avais volontairement omis l'attribut « auteur », qui est le pseudo de celui qui a écrit l'article. Souvenez-vous, pour commencer, on n'a pas d'entité Utilisateur, on doit donc écrire le pseudo de l'auteur en dur dans les articles. Ajoutez donc (à la main, vous ne pouvez plus utiliser le générateur) les attributs « auteur », de type string, sans oublier le getter et le setter.

Il manque également l'entité Tag. On va la faire très simple : un tag n'aura qu'un attribut utile, son nom. Créez l'entité indépendamment de la relation qu'elle aura avec Article, vous pouvez le faire avec le générateur.

Maintenant, établissez la relation entre Article et Tag. Je vous laisse réfléchir pour identifier le type de relation, et pour déterminer qui en est le propriétaire. Définissez la propriété de relation et le getter/setter qui va bien avec.

N'oubliez pas de mettre à jour la base de données avec les commandes php app/console doctrine:schema:update --dump-sql puis php app/console doctrine:schema:update --force.

Puis, allez remplir des données dans la table avec phpMyAdmin, le contrôleur ArticleController généré ne pouvant pas gérer de quoi ajouter des tags à un article. Rappelez-vous, il n'est qu'une base pour notre code, pas la solution toute faite. ;)

Enfin, ajoutez une méthode dans l'ArticleRepository pour récupérer tous les articles qui correspondent à une liste de tags. La définition de la méthode est donc <?php getAvecTags(array $nom_tags) ?>, que l'on pourra utiliser comme cela par exemple : <?php $articleRepository->getAvecTags(array('sdz', 'weekend')) ?>.

À vous de jouer !


Important : faites-le vous-même ! La correction est juste en dessous, je sais, mais si vous ne faites pas maintenant l'effort d'y réfléchir par vous-même, cela vous handicapera par la suite !


Le code


Article.php :
Code : PHP
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
<?php
// src/Sdz/BlogBundle/Entity/Article.php

namespace Sdz\BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

use Sdz\BlogBundle\Entity\Tag;

/**
 * Sdz\BlogBundle\Entity\Article
 *
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="Sdz\BlogBundle\Entity\ArticleRepository")
 */
class Article
{
    /**
     * @var integer $id
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var Datetime $date
     *
     * @ORM\Column(name="date", type="date")
     */
    private $date;

    /**
     * @var string $titre
     *
     * @ORM\Column(name="titre", type="string", length=255)
     */
    private $titre;
    
    /**
     * @var text $contenu
     *
     * @ORM\Column(name="contenu", type="text")
     */
    private $contenu;

    /**
     * @var string $auteur
     *
     * @ORM\Column(name="auteur", type="string", length=255)
     */
    private $auteur;
    
    /**
     * @ORM\ManyToMany(targetEntity="Sdz\BlogBundle\Entity\Tag")
     */
    private $tags;

    public function __construct()
    {
        $this->tags = new ArrayCollection();
    }

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set date
     *
     * @param \Datetime $date
     */
    public function setDate(\Datetime $date)
    {
        $this->date = $date;
    }

    /**
     * Get date
     *
     * @return \Datetime 
     */
    public function getDate()
    {
        return $this->date;
    }

    /**
     * Set titre
     *
     * @param string $titre
     */
    public function setTitre($titre)
    {
        $this->titre = $titre;
    }

    /**
     * Get titre
     *
     * @return string 
     */
    public function getTitre()
    {
        return $this->titre;
    }

    /**
     * Set contenu
     *
     * @param text $contenu
     */
    public function setContenu($contenu)
    {
        $this->contenu = $contenu;
    }

    /**
     * Get contenu
     *
     * @return text 
     */
    public function getContenu()
    {
        return $this->contenu;
    }

    /**
     * Set auteur
     *
     * @param string $auteur
     */
    public function setAuteur($auteur)
    {
        $this->auteur = $auteur;
    }

    /**
     * Get auteur
     *
     * @return string 
     */
    public function getAuteur()
    {
        return $this->auteur;
    }
    
    /**
     * Add tag
     *
     * @param Tag $tag
     */
    public function addTag(Tag $tag)
    {
        $this->tags[] = $tag;
    }

    /**
     * Get tags
     *
     * @return ArrayCollection
     */
    public function getTags()
    {
        return $this->tags;
    }
}

Notez que j'ai forcé la variable « date » à être une instance de \Datetime. En effet, c'est ce que Doctrine utilise, c'est ce qu'il va nous retourner lorsque l'on fait des requêtes. Donc autant travailler avec un objet \Datetime partout dans notre code, et oublier les chaines de caractères <?php 'dd/mm/YY' ?>, ça fait tellement années 90 ! Et n'ayez crainte, depuis un template Twig, vous pourrez faire {{ article.date|date('dd/mm/YY') }} pour transformer votre \Datetime en quelque chose de lisible pour vos visiteurs.

Pour plus d'informations sur la classe Datetime de PHP, je vous invite à lire sa documentation.

Tag.php :

Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php
// src/Sdz/BlogBundle/Entity/Tag.php

namespace Sdz\BlogBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Sdz\BlogBundle\Entity\Tag
 *
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="Sdz\BlogBundle\Entity\TagRepository")
 */
class Tag
{
    /**
     * @var integer $id
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string $nom
     *
     * @ORM\Column(name="nom", type="string", length=255)
     */
    private $nom;


    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set nom
     *
     * @param string $nom
     */
    public function setNom($nom)
    {
        $this->nom = $nom;
    }

    /**
     * Get nom
     *
     * @return string 
     */
    public function getNom()
    {
        return $this->nom;
    }
}


ArticleRepository.php :
Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
// src/Sdz/BlogBundle/Entity/ArticleRepository.php

namespace Sdz\BlogBundle\Entity;

use Doctrine\ORM\EntityRepository;

/**
 * ArticleRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
class ArticleRepository extends EntityRepository
{
    public function getAvecTags(array $nom_tags)
    {
        $qb = $this->createQueryBuilder('a');

        // On fait une jointure sur la table des tags, avec pour alias « t ».
        $qb ->join('a.tags', 't')
            ->where($qb->expr()->in('t.nom', $nom_tags)); // Puis on filtre sur le nom des tags.

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


Que faire avec ce que retourne cette fonction ?

Comme je l'ai dit plus haut, cette fonction va retourner un ArrayCollection d'Article. Qu'est-ce que l'on veut en faire ? Les afficher. Donc la première chose à faire est de passer cet ArrayCollection à Twig. Ensuite, dans Twig, vous faites un simple {% for %} pour afficher les articles. En fait, c'est simple, regardez comment fonctionne la méthode indexAction() du contrôleur Article généré, puis son template en Article:index.html.twig. Ça n'est vraiment pas compliqué à utiliser !

Et voilà, vous avez tout le code. Je n'ai qu'une chose à vous dire à ce stade du tutoriel : entraînez-vous ! Amusez-vous à faire des requêtes dans tous les sens dans l'ArticleRepository ou même dans TagRepository. Jouez avec la relation entre les deux entités, créez-en d'autres. Bref, ça ne viendra pas tout seul, il va falloir travailler un peu de votre côté. ;)
Ce chapitre clôture la partie sur Doctrine2, le prochain étant un chapitre TP pour appliquer ce que vous avez appris. Vous savez maintenant parfaitement gérer vos données grâce à un ORM, et pour cela, toutes mes félicitations !

Pour avoir sous les yeux tout ce qu'il faut savoir sur Doctrine2, je vous propose de télécharger la cheatsheet de Elao, une agence web qui fait du Symfony2. C'est un PDF très pratique qui résume en 2 pages tout ce qu'il y a à savoir sur Doctrine2, vous pouvez le trouver ici : http://www.elao.org/wp-content/uploads [...] sheet-all.pdf
Chapitre précédent Sommaire Chapitre suivant

Partager

25 commentaires pour "Récupérer ses entités avec Doctrine2"
Note moyenne : 3.75 / 4 (245 votes)
Pseudo Commentaire
Hors ligne winzou # Posté le 26/03/2012 à 09:29:48
lala
Avatar

Avis : Très bon Modérateurs

Ville : Singapour
Pays : Singapour
Études : Ecole Centrale de Lyon

Salut,

Pour savoir, il faut tester ;)

Un tutoriel pour débuter avec le framework Symfony2.
Chapitre en beta-test : Déployer son site Symfony2 en production, donnez vos avis !

Je recherche toujours quelqu'un capable de faire des icônes sympas pour les chapitres du tutoriel, contactez-moi, merci !
 
Hors ligne Odinscand # Posté le 21/04/2012 à 14:50:31
Avatar

Salut,

Petite surprise aujourd'hui, en testant "get_class()" sur la valeur de retour d'un findAll(), PHP me fait la tronche et me dit que get_class est réservé aux Object, et non aux Array.
Cela semble supposer que findAll() a renvoyé un Array, et non un ArrayCollection.

Pourtant, ce (très chouette) tuto semble supposer que la valeur de retour des findBy() et findAll() est forcément ArrayCollection.

Une explication...?
Merci !
Hors ligne khadijasup # Posté le 23/04/2012 à 14:16:03 Message supprimé pour le motif suivant : Merci de poster vos problèmes sur le forum, pas ici..

Bonjour,

J'ai un souci concernant une requête dans mon contrôleur;je veux quand je clique sur un lien "categorie" qu'on m'affiche les produits qu'elle contienne.mais une erreur apparait

voici la fonction qui contient la requete Code : PHP
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?php public function FindByCategorieAction($categorie)
	{
		$em = $this->getDoctrine()->getEntityManager();
		
		$Categ= $em->getRepository('Ecommerce\boutiqueBundle\Entity\Categorie')->findBy(array('Libelle'=>$categorie));
			//$Categ= $em->createQuery('SELECT uws FROM MyProject\Model\WebSite ws JOIN ws.domain WHERE ws.domain = ');
		    // On vérifie que le Produit d'id $id existe bien, sinon,erreur 404.
		    if( ! $Produit = $em->getRepository('Ecommerce\boutiqueBundle\Entity\Produit')->findBy( array('Categorie'=>$Categ)) )
		    {
			   throw $this->createNotFoundException('la categorie '.$categorie.' ne contient aucun produit !');
		    }
					
				return $this->render('EcommerceboutiqueBundle:boutique:FindByCategorie.html.twig', array('Produits' => $Produit));
	  
	}


et voila l'erreur qui est générée Notice: Undefined index: Categorie in C:\wamp\www\Symfony21\vendor\doctrine\lib\Doctrine\ORM\Persisters\BasicEntityPersister.php line 1324
500 Internal Server Error - ErrorException
.

Merci pour vos réponses.
Hors ligne ovh # Posté le 25/04/2012 à 20:49:20

Pour que ça retourne systématiquement un ArrayCollection, il faut le définir en tant que tel dans le constructeur ;) Sinon par défaut il renvoit soit une collection, soit un "bête" tableau.

Je n'ai rien à voir avec www.ovh.com
 
Hors ligne aubino24 # Posté le 14/05/2012 à 18:04:30

Bonjour :euh: , je voudrais savoir comment obtenir le dernier enregistrement d'une table :-°

Voir tous les commentaires