Aller au contenu principal
Retour au blog

3 patterns Doctrine que les seniors utilisent (et les juniors ignorent)

Fonctions DQL custom, Read Models avec DTOs et Event Listeners pour audit : 3 patterns Doctrine avancés.

Flavien Métivier · 5 min de lecture
Sommaire · 3 parties

Doctrine est un ORM puissant, mais la plupart des développeurs n'utilisent que 20 % de ses capacités. Voici 3 patterns que les développeurs seniors appliquent systématiquement — et qui peuvent diviser par 10 le temps de certaines requêtes.

Pattern 1 : Read Models avec DTOs (adieu l'hydratation complète)

Le réflexe junior : charger l'entité complète, même quand on n'a besoin que du nom et de l'email. Chaque findAll() hydrate tous les champs, toutes les relations lazy-loaded. C'est du gaspillage de mémoire et de CPU.

php
<?php

// AVANT (junior) — Hydratation complète
$users = $this->userRepository->findAll();
// Charge TOUTES les propriétés, TOUTES les relations
// Résultat : 50+ colonnes hydratées, dont 45 inutiles

foreach ($users as $user) {
    $names[] = $user->getName(); // On voulait juste ça
}
php
<?php

// APRÈS (senior) — DTO projection
final readonly class UserListDTO
{
    public function __construct(
        public string $id,
        public string $name,
        public string $email,
    ) {}
}

// Dans le repository
public function findAllForList(): array
{
    return $this->createQueryBuilder('u')
        ->select('NEW App\\Presentation\\DTO\\UserListDTO(u.id, u.name, u.email)')
        ->getQuery()
        ->getResult();
    // Résultat : 3 colonnes, pas d'hydratation Entity
    // Performance : 3-10x plus rapide sur des tables volumineuses
}

La syntaxe NEW de DQL crée directement des DTOs sans passer par l'hydratation d'entité. C'est le pattern Read Model du CQRS appliqué à Doctrine.

Pattern 2 : Eager loading intelligent (zéro N+1)

Le problème N+1 est le tueur de performance numéro 1. Tu charges 100 commandes, puis pour chacune, Doctrine fait une requête pour charger les items. Résultat : 101 requêtes au lieu d'une seule.

php
<?php

// AVANT (junior) — N+1 queries
$orders = $this->orderRepository->findAll();
// 1 requête

foreach ($orders as $order) {
    $items = $order->getItems(); // +1 requête par commande !
    foreach ($items as $item) {
        $product = $item->getProduct(); // +1 requête par item !!
    }
}
// Total : 1 + 100 + 500 = 601 requêtes

Migration Symfony en vue ?

Bear Upgrade migre votre codebase 2,5× à 4,5× moins cher qu'une ESN, selon sa maturité.

Estimer ma migration →
php
<?php

// APRÈS (senior) — JOIN fetch en une requête
public function findWithRelations(): array
{
    return $this->createQueryBuilder('o')
        ->select('o', 'items', 'product')
        ->leftJoin('o.items', 'items')
        ->leftJoin('items.product', 'product')
        ->getQuery()
        ->getResult();
    // Total : 1 requête avec JOINs
    // Performance : de 601 requêtes à 1
}

La clé : toujours inclure les relations dans le SELECT. ->select('o', 'items', 'product') force Doctrine à tout charger en une seule requête.

Pattern 3 : Event Listeners pour audit et soft delete

Le réflexe junior : ajouter du code d'audit manuellement dans chaque service. Le senior utilise les lifecycle events de Doctrine pour centraliser cette logique.

php
<?php

declare(strict_types=1);

namespace App\Infrastructure\Doctrine\EventListener;

use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Events;
use Psr\Log\LoggerInterface;

#[AsDoctrineListener(event: Events::preUpdate)]
final readonly class AuditListener
{
    public function __construct(
        private LoggerInterface $auditLogger,
    ) {}

    public function preUpdate(PreUpdateEventArgs $args): void
    {
        $entity = $args->getObject();
        $changes = $args->getEntityChangeSet();

        $this->auditLogger->info('Entity updated', [
            'class' => $entity::class,
            'id' => method_exists($entity, 'id') ? (string) $entity->id() : '?',
            'changes' => array_keys($changes),
        ]);
    }
}

Avec les PHP 8 Attributes et #[AsDoctrineListener], plus besoin de configuration YAML. Le listener est automatiquement détecté par l'autowiring.

Ces 3 patterns sont la base d'un projet Doctrine performant. Si tu veux un audit complet de tes requêtes Doctrine et de ton architecture Symfony, Bear Upgrade inclut une analyse détaillée avec des recommandations chiffrées.

Cet article vous a été utile ? Partagez-le.

Portrait de Flavien Métivier, fondateur de The Bearded Bear : barbe longue, lunettes, sur fond sombre

Écrit par

Flavien Métivier

Expert PHP/Symfony indépendant depuis 2014, ancien CTO de Kartable et d'Extrabat. Plus de 80 projets menés, de l'audit à la migration.

Mon parcours

À lire ensuite

Migration Symfony en vue ?

Bear Upgrade migre votre codebase 2,5× à 4,5× moins cher qu'une ESN, selon sa maturité.

Newsletter Bear Bytes

Veille Claude Code, Symfony et PHP, retours d'expérience terrain et conseils techniques, directement dans votre boîte mail.