Tous droits réservés

À la rencontre du QueryBuilder

Le langage SQL possède un grand nombre d’opérations. En une requête, nous pouvons calculer des sommes, des moyennes, faire des jointures entre différentes tables, gérer des transactions, etc.

À ce stade, il nous manque beaucoup de fonctionnalités du langage SQL que les méthodes simples de la famille findXXX ne peuvent pas fournir.

Dans cette partie, nous allons voir un ensemble de concepts qui nous permettrons de rendre une application utilisant Doctrine fonctionnelle, efficace et performante.

Le QueryBuilder

Le QueryBuilder 1 est une classe permettant de créer des requêtes en utilisant le langage PHP. Pour avoir un aperçu de cette classe, voici un extrait issu de la documentation officielle de Doctrine de son API.

<?php
class QueryBuilder
{
    public function select($select = null);

    public function addSelect($select = null);

    public function delete($delete = null, $alias = null);

    public function update($update = null, $alias = null);

    public function set($key, $value);

    public function from($from, $alias, $indexBy = null);

    public function join($join, $alias, $conditionType = null, $condition = null, $indexBy = null);

    public function innerJoin($join, $alias, $conditionType = null, $condition = null, $indexBy = null);

    public function leftJoin($join, $alias, $conditionType = null, $condition = null, $indexBy = null);

    public function where($where);

    public function andWhere($where);

    public function orWhere($where);

    public function groupBy($groupBy);

    public function addGroupBy($groupBy);

    public function having($having);

    public function andHaving($having);

    public function orHaving($having);

    public function orderBy($sort, $order = null);

    public function addOrderBy($sort, $order = null); 
}

À la lecture de l’API de cette classe, nous pouvons retrouver pas mal de mots clés SQL dans les méthodes disponibles (SELECT, UPDATE, DELETE, GROUP BY, HAVING, etc.). En effet, cette classe rajoute une couche d’abstraction entre notre code PHP et les requêtes SQL effectives que nous voulons exécuter.

Doctrine se charge ensuite de traduire la requête dans la bonne syntaxe SQL en prenant bien sûr en compte le moteur de base de données que nous utilisons (MySQL, PostgreSQL, MSSQL, Oracle, etc.).

Création d’un QueryBuilder

La meilleure façon d’appréhender le mode de fonctionnement du QueryBuilder est d’utiliser des exemples.

<?php
# query-builder.php

$entityManager = require_once join(DIRECTORY_SEPARATOR, [__DIR__, 'bootstrap.php']);

use Tuto\Entity\User;

$userRepo = $entityManager->getRepository(User::class);

$queryBuilder = $entityManager->createQueryBuilder();

$queryBuilder->select('u')
   ->from(User::class, 'u')
   ->where('u.firstname = :firstname')
   ->setParameter('firstname', 'First');

$query = $queryBuilder->getQuery();

echo $query->getDQL(), "\n";
echo $query->getOneOrNullResult();

Une fois n’est pas de coutume. L'entity manager est utilisé pour créer notre QueryBuilder. Ensuite, nous construisons une requête SQL avec du code PHP.

SELECT u FROM Tuto\Entity\User u WHERE u.firstname = :firstname
User (id: 1, firstname: First, lastname: LAST, role: admin, address: Address 
(id: 1, street: Champ de Mars, 5 Avenue Anatole, city: Paris, country: France))

La première ligne du résultat a des airs de famille avec le SQL. Au lieu d’avoir le nom du table, nous avons le nom d’une entité. L’alias « u » est utilisé pour récupérer tous les attributs de l’entité (équivalent du SELECT *). Le langage utilisé s’appelle le DQL : Doctrine Query Langage.

C’est un langage de requête conçu à l’image de Hibernate Query Language (HQL), un ORM pour Java, qui permet de faire des requêtes en utilisant nos classes PHP.

Il faut voir le DQL comme étant une couche par-dessus le SQL qui utilise nos entités comme des tables et les attributs de ceux-ci comme des noms de colonne. Il permet de réaliser ainsi presque toutes les requêtes que supporte le SQL natif en se basant exclusivement sur notre modèle objet.

La configuration du QueryBuilder supporte le système de paramètres nommés à l’image de PDO. Son utilisation est très grandement recommandée pour éviter les injections SQL.

Il est aussi intéressant de souligner que pour gérer les paramètres nommés passés au QueryBuilder, nous avons le choix entre des entiers qui doivent être préfixés par le caractère « ? », ou des chaînes de caractères qui doivent être préfixées par le caractère « : ».

Exemples de requêtes avec le QueryBuilder

Pour mieux appréhender son comportement, nous allons voir quelques exemples de requêtes basées sur le QueryBuilder.

Suppression d’un utilisateur

<?php
$queryBuilder = $entityManager->createQueryBuilder();
$queryBuilder->delete(User::class, 'u')
   ->where('u.id = :id')
   ->setParameter('id', 5);

$query = $queryBuilder->getQuery();

echo $query->getDQL(), "\n";
echo $query->execute();

Résultat :

DELETE Tuto\Entity\User u WHERE u.id = :id
1 # nombre de lignes supprimées

Recherche d’un utilisateur

<?php
$queryBuilder = $entityManager->createQueryBuilder();
$queryBuilder->select('u')
   ->from(User::class, 'u')
   ->where('u.firstname LIKE :firstname')
   ->andWhere('u.lastname = :lastname')
   ->setParameter('firstname', 'First %')
   ->setParameter('lastname', 'LAST 3');

$query = $queryBuilder->getQuery();

echo $query->getDQL(), "\n";
foreach ($query->getResult() as $user) {
    echo $user;
}

Résultat :

SELECT u FROM Tuto\Entity\User u WHERE u.firstname LIKE :firstname AND u.lastname = :lastname
User (id: 4, firstname: First 3, lastname: LAST 3, role: user, address: )

Recherche de plusieurs utilisateurs

<?php
$queryBuilder = $entityManager->createQueryBuilder();
$queryBuilder->select('u')
   ->from(User::class, 'u')
   ->where('u.id IN (:ids)')
   ->setParameter('ids', [3,4]);

$query = $queryBuilder->getQuery();

echo $query->getDQL(), "\n";
foreach ($query->getResult() as $user) {
    echo $user;
}

Résultat :

SELECT u FROM Tuto\Entity\User u WHERE u.id IN (:ids)
User (id: 3, firstname: First 2, lastname: LAST 2, role: user, address: )
User (id: 4, firstname: First 3, lastname: LAST 3, role: user, address: )

Recherche de plusieurs utilisateurs avec des limitations

<?php
$queryBuilder = $entityManager->createQueryBuilder();
$queryBuilder->select('u')
   ->from(User::class, 'u')
   ->setFirstResult(5)
   ->setMaxResults(3);

$query = $queryBuilder->getQuery();

echo $query->getDQL(), "\n";
foreach ($query->getResult() as $user) {
    echo $user;
}

Ici, nous affichons aussi le SQL pour bien voir l’utilisation des limitations (LIMIT et OFFSET).

SQL: SELECT u0_.id AS id_0, u0_.firstname AS firstname_1, u0_.lastname AS lastname_2, u0_.role AS role_3, 
u0_.address_id AS address_id_4 FROM users u0_ LIMIT 3 OFFSET 5
DQL: SELECT u FROM Tuto\Entity\User u
User (id: 8, firstname: First 7, lastname: LAST 7, role: user, 
address: Address (id: 17, street: Place d'Armes, city: Versailles, country: France))
User (id: 9, firstname: First 8, lastname: LAST 8, role: user, address: )
User (id: 10, firstname: First 9, lastname: LAST 9, role: user, address: )

Mise à jour d’un utilisateur

<?php
$address = new Address();
$address->setStreet("Place d'Armes");
$address->setCity("Versailles");
$address->setCountry("France");
$entityManager->persist($address);
$entityManager->flush();

$queryBuilder = $entityManager->createQueryBuilder();
$queryBuilder->update(User::class, 'u')
   ->set('u.address', '?1')
   ->where('u.id = ?2')
   ->setParameter(1, $address->getId())
   ->setParameter(2, 8);

$query = $queryBuilder->getQuery();

echo $query->getDQL(), "\n";
echo $query->execute(), "\n";
echo $userRepo->find(8), "\n";

Résultat :

UPDATE Tuto\Entity\User u SET u.address = ?1 WHERE u.id = ?2
1 # nombre de lignes mises à jour
User (id: 8, firstname: First 7, lastname: LAST 7, role: user, 
address: Address (id: 2, street: Place d'Armes, city: Versailles, country: France))

Récupérer le nombre d’utilisateurs dans la base

<?php
$queryBuilder = $entityManager->createQueryBuilder();

$queryBuilder->select('COUNT(u.id)')
    ->from(User::class, 'u');

$query = $queryBuilder->getQuery();

echo $query->getDQL(), "\n";
echo $query->getSingleScalarResult();

Résultat :

SELECT COUNT(u.id) FROM Tuto\Entity\User u
10 # 10 utilisateurs dans notre base de données

Sans le QueryBuilder, nous étions obligés de récupérer une entité utilisateur avant de pouvoir la modifier ou la supprimer. Ce qui impliquait l’exécution d’au moins deux requêtes.

Maintenant, nous avons tous les outils nécessaires pour éditer une ou plusieurs entités en une seule requête, limiter le nombre de résultat avec les mèthodes setFirstResult et setMaxResults ou encore faire des jointures à l’image du langage SQL.

Il est possible d’exécuter directement du DQL en utilisant la méthode createQuery de l'entity manager. Mais nous n’aborderons pas ce cas d’usage. La documentation officielle comporte beaucoup d’exemples sur ce sujet.

Personnaliser les réponses du QueryBuilder

À chaque requête, nous avons le choix du type de réponses que nous souhaitons. Ainsi, suivant la méthode utilisée, Doctrine peut nous envoyer une entité, une liste, un tableau ou même un scalaire.

Méthode appelée Retour
$query->getResult() Une liste d’entités ou le nombre de lignes mis à jour par la requête (mise à jour ou suppression)
$query->getSingleResult() Une seule entité (lève une exception si le résultat est nul ou contient plusieurs entités)
$query->getOneOrResult() Une seule entité (renvoie null si la réponse est vide, et léve une exception si elle contient plusieurs entités)
$query->getArrayResult() Une liste de tableaux
$query->getScalarResult() Une liste de scalaires
$query->getSingleScalarResult() Un seul scalaire (lève une exception si le résultat est nul ou contient plusieurs scalaires)

Même si le besoin est rare, nous pouvons quand même personnaliser encore plus les réponses du QueryBuilder. Les méthodes getResult, getSingleResult, getOneOrResult acceptent un paramètre hydrationMode qui permet de choisir entre une entité (choix par défaut) ou un tableau. Pour récupérer, par exemple, les informations d’un seul utilisateur en tant que tableau, nous pouvons écrire :

<?php
$queryBuilder = $entityManager->createQueryBuilder();

$queryBuilder->select('u')
   ->from(User::class, 'u')
   ->where('u.id = :id')
   ->setParameter('id', 1);

$query = $queryBuilder->getQuery();

var_dump($query->getSingleResult(Doctrine\ORM\Query::HYDRATE_ARRAY));

Nous obtenons un tableau associatif avec comme clé les noms des attributs.

array(4) {
  ["id"]=>
  int(1)
  ["firstname"]=>
  string(5) "First"
  ["lastname"]=>
  string(4) "LAST"
  ["role"]=>
  string(5) "admin"
}

Les expressions

Dans tous nos exemples, nous avons utilisé des mots clés SQL comme LIKE, COUNT, etc. Doctrine nous permet de nous affranchir de cela en utilisant les expressions du QueryBuilder.

Voici un exemple d’utilisation pour le COUNT :

<?php
$queryBuilder = $entityManager->createQueryBuilder();

$queryBuilder->select($queryBuilder->expr()->count('u.id'))
    ->from(User::class, 'u');

$query = $queryBuilder->getQuery();

echo $query->getDQL(), "\n";
echo $query->getSingleScalarResult(), "\n";

Même si le code change, la requête générée reste néanmoins la même :

SELECT COUNT(u.id) FROM Tuto\Entity\User u

Vous pouvez consulter l’API de Doctrine pour voir toutes les méthodes disponibles.

Les expressions sont souvent qualifiées de complexes pour le bénéfice qu’il rajoute au code. Il est donc courant de voir des applications utilisant Doctrine sans ce composant.

Nous avons créé beaucoup de requêtes qui pourraient être utiles dans différentes parties de notre code. Comment faire pour réutiliser les requêtes avancées que nous venons de créer ?

Nous allons répondre à cette question en explorant les repositories personnalisés de Doctrine.


  1. Le terme QueryBuilder signifie littéralement « constructeur de requêtes ».

Les repositories personnalisés

Configuration

Lorsque nous avions fait appel à l'entity manager pour récupérer un repository, Doctrine avait créé pour nous un repository qui dispose nativement d’un ensemble de méthodes utilitaires pour faire des requêtes de base.

Cependant, nous avons la possibilité, pour chaque entité, de personnaliser le repository que Doctrine va utiliser.

La configuration de celui-ci se fait en deux temps :

  1. Pour commencer, il faut créer une classe qui étend la classe EntityRepository ;
  2. Ensuite, il faut demander à Doctrine d’utiliser ce repository.

Nous allons tester cette configuration avec les utilisateurs. Le repository ressemble donc à :

<?php
# src/Repository/UserRepository.php

namespace Tuto\Repository;

use Doctrine\ORM\EntityRepository;

class UserRepository extends EntityRepository
{
}

Pour l’utiliser, nous devons mettre à jour les annotations sur l’entité utilisateur. Vous l’aurez peut-être remarqué, les entités représente la glu entre notre code et Doctrine. Toutes les configurations se situent sur celles-ci. Cela permet de centraliser tout ce qui est lié à l’ORM.

Pour déclarer un repository, il suffit juste d’utiliser l’attribut repositoryClass de l’annotation Entity.

<?php
# src/Entity/User.php

namespace Tuto\Entity;

use Tuto\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity(repositoryClass=UserRepository::class)
* // ...
*/
class User
{
   // ...
}

Nous pouvons maintenant tester cette configuration.

<?php
# custom-repository.php

$entityManager = require_once join(DIRECTORY_SEPARATOR, [__DIR__, 'bootstrap.php']);

use Tuto\Entity\User;
use Tuto\Entity\Poll;

$userRepo = $entityManager->getRepository(User::class);
echo get_class($userRepo), "\n";

$pollRepo = $entityManager->getRepository(Poll::class);
echo get_class($pollRepo), "\n";

Résultat :

Tuto\Repository\UserRepository # Notre classe est utilisée.
Doctrine\ORM\EntityRepository

Notre repository est bien récupéré par la méthode getRepository. Et puisque nous étendons la classe EntityRepository, les méthodes déjà vues durant ce cour sont tous valides.

Utilisation des repositories personnalisés

Ce repository peut maintenant être utilisé à la place du repository natif de Doctrine. Dans l’API de celui, nous disposons de plusieurs méthodes qui nous permettent d’accéder indirectement à l'entity manager.

Toutes les requêtes basées sur le QueryBuilder peuvent être ainsi encapsulées dans le repository. Le code utilisé reste identique à ce que nous avons déjà utilisé pour illustrer l’API du QueryBuilder.

<?php
# src/Repository/UserRepository.php

namespace Tuto\Repository;

use Doctrine\ORM\EntityRepository;
use Tuto\Entity\User;

class UserRepository extends EntityRepository
{
    public function searchByFirstname($firstname)
    {
        $queryBuilder = $this->_em->createQueryBuilder()
           ->select('u')
           ->from(User::class, 'u')
           ->where('u.firstname = :firstname')
           ->setParameter('firstname', $firstname);

        $query = $queryBuilder->getQuery();

        return $query->getOneOrNullResult();
    }

    public function deleteById($id)
    {
        $queryBuilder = $this->_em->createQueryBuilder();
        $queryBuilder->delete(User::class, 'u')
           ->where('u.id = :id')
           ->setParameter('id', $id);

        $query = $queryBuilder->getQuery();

        return $query->execute();
    }
}

Maintenant à chaque fois que nous accédons au repository, nous pouvons utiliser ces méthodes.

<?php
# custom-repository.php

$entityManager = require_once join(DIRECTORY_SEPARATOR, [__DIR__, 'bootstrap.php']);

use Tuto\Entity\User;

$userRepo = $entityManager->getRepository(User::class);

echo $userRepo->searchByFirstname("First");
User (id: 1, firstname: First, lastname: LAST, role: admin, 
address: Address (id: 1, street: Champ de Mars, 5 Avenue Anatole, city: Paris, country: France))

N’hésitez pas à implémenter les autres méthodes pour vous entrainer.

Il est certes possible d’utiliser directement l'entity manager pour créer des QueryBuilder. Mais en les mettant dans des repositories personnalisés, nous pouvons centraliser toutes les requêtes et les réutiliser sans duplication de code.


Le QueryBuilder offre une API puissante qui nous permet de tirer un maximum de profit des fonctionnalités de notre base de données. Son principe d’utilisation est simple et proche du SQL natif. Vous ne devriez donc pas être trop perdu en l’utilisant dans vos applications.

L’ORM ne représente malheureusement pas la solution miracle et il existe des cas où utiliser des requêtes SQL natives sera inévitable.

Donc, si ce qui est présenté ne remplit pas entièrement votre besoin, il faut savoir qu’il est possible avec Doctrine d'exécuter des requête SQL natives et même d’associer le résultat à un objet.