Le mapping par attributs PHP
Depuis Doctrine ORM 2.9+ et surtout en version 3.x, le mapping par attributs PHP 8 remplace les annotations Doctrine et le YAML/XML. Il n'y a plus besoin de parser des docblocks : les métadonnées sont natives au langage.
#[ORM\Entity]
#[ORM\Table(name: 'articles')]
class Article
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private int $id;
#[ORM\Column(length: 255)]
private string $titre;
#[ORM\ManyToOne(targetEntity: Auteur::class, inversedBy: 'articles')]
private Auteur $auteur;
}
Unit of Work : ce qui se passe vraiment à flush()
Doctrine ne persiste rien immédiatement. L'EntityManager délègue à l'Unit of Work, qui garde un instantané (snapshot) de chaque entité chargée. À l'appel de flush(), Doctrine compare l'état actuel des objets à ce snapshot pour calculer un changeset, puis génère les INSERT/UPDATE/DELETE dans le bon ordre topologique selon les dépendances entre entités.
Conséquence pratique : modifier une propriété d'une entité managée suffit, pas besoin de rappeler persist(). À l'inverse, oublier flush() ne fait strictement rien en base, même si les objets PHP semblent à jour.
Le piège des collections détachées
Une entité récupérée dans une requête puis modifiée après un clear() ou en dehors du cycle de vie du manager devient détachée : ses changements ne sont plus suivis. Il faut alors la re-fusionner explicitement, ou mieux, restructurer le code pour rester dans une seule unité de travail cohérente.
Lazy loading et proxies : attention au N+1
Par défaut, les associations ManyToOne/OneToMany sont chargées en lazy via des proxies générés dynamiquement (classes enfants qui interceptent le premier accès pour déclencher une requête). C'est pratique, mais source classique du problème N+1 : itérer sur 100 articles et accéder à $article->getAuteur()->getNom() déclenche 100 requêtes supplémentaires.
La solution est le eager fetching ciblé via DQL :
$qb = $em->createQueryBuilder();
$qb->select('a', 'auteur')
->from(Article::class, 'a')
->join('a.auteur', 'auteur');
Le double select('a', 'auteur') est essentiel : sans lui, la jointure SQL existe mais Doctrine n'hydrate que Article, et l'accès à l'auteur redéclenche une requête lazy.
Bonnes pratiques d'audit
- Activer le SQL Logger ou le profiler Symfony pour repérer les requêtes dupliquées en boucle.
- Préférer des DTO/projections (
SELECT NEWen DQL) pour les listes en lecture seule, plutôt que d'hydrater des entités complètes. - Utiliser
Criteriasur les collections pour filtrer sans forcer un chargement complet.