Doctrine : créer un DTO depuis le Query Builder

Retour

Si tu utilises Symfony, tu as surement déjà réalisé une requête à l’aide du Query Builder. Le résultat de la dite requête sera un objet (une entité) ou un tableau d’objet. Si par contre tu fais un select pour récupérer seulement quelques champs, tu te retrouveras avec un tableau associatif. Pas pratique à manipuler. Dans un précédent article, je mentionnais déjà l’utilisation des DTO pour la validation de données provenant (par exemple) d’une requête API. Cette fois on va s’en servir dans l’autre sens : instancier une classe DTO avec les données d’une requête SQL.

Query Builder

Ecrivons une simple requête à l’aide du Query Builder pour récupérer les produits ayant une quantité supérieur à zéro :

$qb = $this->createQueryBuilder('p');

$qb->andWhere(
    $qb->expr()->gt('p.quantity', ':gt')
);

$qb->setParameter('gt', 0);

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

Si on dump le résultat de cette requête, on aura donc un tableau de Product. Toutes les propriétés seront récupérées mais pour notre feature, on en a pas forcement besoin.

On va ajouter un select() pour récupérer seulement les colonnes qui nous intéressent :

$qb->select(
    '
    p.id,
    p.name,
    p.price,
    p.priceId
    '
);

Nice ! Mais cette fois Doctrine nous retourne un tableau associatif. Alors comment conserver un objet ?

Dans un cas où nous développerions une API, on pourrait utiliser les groupes de serialization pour réduire notre objet. Mais admettons que nous souhaiterions aussi changer le nommage. C’est là que le DTO entre en action.

L’opérateur NEW

Doctrine nous permet de créer un DTO directement depuis notre requête. Cela est possible en utilisant l’opérateur NEW à l’intérieur du select en spécifiant la classe de notre DTO. Pour réaliser ce trick, il faut ajouter un constructeur dans notre DTO :

class ProductDTO
{
    public function __construct(
        public int $id,
        public string $name,
        public float $price,
        public ?string $priceId
    ) {
    }
}

Il ne reste plus qu’à modifier notre select de cette manière :

$qb->select(sprintf(
    'NEW %s(
        p.id,
        p.name,
        p.price,
        p.priceId
    )', ProductDTO::class
));

⚠️ Avec cette syntaxe, il faudra veiller à conserver le même ordre de champs que dans le constructeur.

Et voilà ! Désormais notre Query Builder retourne un tableau de ProductDTO. Tu peux également te servir de cette syntaxe en DQL et pour le coup tu n’auras pas besoin de mapper la requête à une entité.