Hydratez le résultat de vos requêtes Doctrine dans un DTO

Introduction

Par default Doctrine vous retourne le résultat de vos requête soit dans un tableau d'entité, soit dans un tableau à plusieurs dimensions selon le mode d'hydratation que vous avez choisi (HYDRATE_OBJECT ou HYDRATE_ARRAY).

L'hydratation en tableau est souvent utilisé pour des gains de performance mais à le désavantage de perdre le coté objet. L'autre option est d'hydrater des objets conçu pour contenir le résultat de ces requêtes : les DTOs.

L'opérateur NEW

Avant il était nécessaire de passer par Hydrator personnalisé pour obtenir ce résultat. Heureusement il existe depuis depuis la version 2.4 de Doctrine un nouvel opérateur NEW permettant d'indiquer à Doctrine la classe d'objet à hydrater. Vous pouvez ainsi hydrater directement vos DTOs. Un vrai plus pour les adeptes de DDD.

Il s'utilise de la façon suivante :

$this
    ->get('doctrine.orm.entity_manager')
    ->createQueryBuilder()
    ->select('NEW Acme\DTO\CategoryListView(category.id, category.title)')
    ->from('AcmeDemoBundle:Category', 'category');

Pour fonctionner, la signature du constructeur de la classe CategoryListView doit correspondre aux champs sélectionnés dans la requête.

namespace Acme\DTO;

class CategoryListView
{
    public $id;
    public $title;

    public function __construct($id, $title)
    {
        $this->id = $id;
        $this->title = $title;
    }
}

Autres exemples

Vous pouvez également ajouter les données provenant d'une jointure :

$this
    ->get('doctrine.orm.entity_manager')
    ->createQueryBuilder()
    ->select('NEW Acme\DTO\CategoryListView(category.id, category.title, category.title)')
    ->leftJoin('article.category', 'category')
    ->from('AcmeDemoBundle:Category', 'category');
class CategoryListView
{
    public function __construct($id, $title, $categoryTitle) { }
}

Mélanger scalaires et entités :

$this
    ->get('doctrine.orm.entity_manager')
    ->createQueryBuilder()
    ->select('NEW Acme\DTO\CategoryListView(category.id, category.title, category)')
    ->leftJoin('article.category', 'category')
    ->from('AcmeDemoBundle:Category', 'category');
class CategoryListView
{
    public function __construct($id, $title, Category $category) { }
}

Ou inclure le résultat d'une sous requête :

$this
    ->get('doctrine.orm.entity_manager')
    ->createQueryBuilder()
    ->select('NEW Acme\DTO\CategoryListView(category.id, category.title, (SELECT COUNT(*) FROM ...) as total)')
    ->leftJoin('article.category', 'category')
    ->from('AcmeDemoBundle:Category', 'category');
class CategoryListView
{
    public function __construct($id, $title, $count) { }
}

Conclusion

Grâce à cette méthode, vous pouvez sélectionner seulement les données dont vous avez besoin afin d'hydrater des DTOs et conservé l'usage d'objet. Pour les adeptes du DDD, cette méthode permet d'utiliser des objets métier différents pour la lecture et l'écriture des données, l'un des préceptes du Domain Driven Design.

Les commentaires