Image de couverture : PHP 8.4 : les lazy objects natifs vont transformer Doctrine et Symfony — et si on mesurait l'impact ?
tech

PHP 8.4 : les lazy objects natifs vont transformer Doctrine et Symfony — et si on mesurait l'impact ?

18 April 2026
5 min de lecture
3 vues
Sébastien Muler

Une annonce à ne pas rater

Benjamin Eberlei (Tideways) présentera à SymfonyLive Berlin 2026 (23-24 avril) une conférence intitulée "How native lazy objects will change Doctrine and Symfony forever". Le sujet : la nouvelle API native de PHP 8.4 pour le lazy loading et les proxy objects, sans génération de code, sans overhead artificiel.

C'est une évolution structurelle qui nous concerne directement chez MulerTech, où Doctrine ORM est au cœur de la quasi-totalité de nos projets Symfony. Avant même de voir les slides, on a voulu poser les bases d'un protocole de mesure sérieux.


Ce que PHP 8.4 change concrètement

Jusqu'ici, le lazy loading dans Doctrine reposait sur la génération de classes proxy au moment du cache:warmup. Ces proxies héritent de l'entité, surchargent les accesseurs et déclenchent le chargement depuis la base de données à la première utilisation. Le mécanisme fonctionne, mais il a un coût :

  • fichiers proxy générés sur disque ou en mémoire
  • overhead à l'instanciation et à l'autoload
  • complexité lors du débogage (la stack trace pointe sur le proxy, pas l'entité)
  • friction avec la sérialisation et certains listeners Doctrine

PhP 8.4 introduit ReflectionClass::newLazyGhost() et ReflectionClass::newLazyProxy(), deux mécanismes natifs qui permettent de créer un objet dont l'initialisation est différée jusqu'au premier accès à une propriété, sans sous-classer l'entité. Le moteur PHP gère lui-même l'état "non initialisé" au niveau du zval. Résultat attendu : moins d'allocations mémoire, pas de génération de fichiers, et un code de débogage bien plus lisible.

Pour Symfony, l'impact concerne aussi le container de services : les services lazys sont aujourd'hui gérés via un bundle dédié et de la génération de proxies. La même simplification est envisageable.


Points de vigilance avant de migrer

L'enthousiasme est légitime, mais quelques zones méritent une attention particulière avant de basculer en production.

Listeners et event subscribers Doctrine Les listeners postLoad, preUpdate ou les subscribers qui inspectent des propriétés d'une entité peuvent se déclencher différemment si l'initialisation est encore différée au moment de l'événement. Il faudra valider le comportement de chaque listener critique sur des entités lazy natives.

Sérialisation Les objets PHP non initialisés ont un comportement spécifique avec serialize(), json_encode() ou des librairies comme JMS Serializer. Un objet ghost dont les propriétés sont dans un état uninitialized peut produire des données incomplètes ou lever une exception selon le sérialiseur. Ce point est à tester systématiquement.

Compatibilité des bundles tiers Certains bundles qui inspectent les métadonnées Doctrine ou wrappent les entités (API Platform, EasyAdmin, etc.) reposent sur la détection de proxies via instanceof Proxy. Ce contrat implicite devra être révisé.

Couverture de tests Un test qui instancie une entité directement sans passer par l'EntityManager ne verra aucune différence. Les gains (et les régressions) n'apparaissent qu'avec un vrai cycle de vie Doctrine. Les tests d'intégration sont indispensables.


Protocole de mesure avec Docker Compose

Pour chiffrer objectivement les gains, voici le protocole que nous allons appliquer sur un projet MulerTech représentatif.

Stack de référence

# docker-compose.yml
services:
  app_php83:
    image: php:8.3-fpm
    # ... configuration Symfony + Doctrine classique

  app_php84:
    image: php:8.4-fpm
    # ... même application, lazy objects natifs activés

  db:
    image: postgres:16
    # dataset de production anonymisé

  blackfire:
    image: blackfire/blackfire:2
    environment:
      BLACKFIRE_SERVER_ID: ${BLACKFIRE_SERVER_ID}
      BLACKFIRE_SERVER_TOKEN: ${BLACKFIRE_SERVER_TOKEN}

Métriques cibles

Indicateur Outil Seuil de validation
Pic mémoire (requête liste) Blackfire / memory_get_peak_usage() − 15 %
TTFB page entité complexe Tideways / curl -w "%{time_starttransfer}" − 10 ms
Temps cache:warmup time bin/console cache:warmup − 30 %
Nombre de fichiers proxy générés find var/cache -name "*Proxy*" | wc -l → 0

Scénario de test

  1. Charger 500 entités avec associations ManyToOne lazy
  2. Accéder à une propriété de l'association sur 10 % d'entre elles seulement
  3. Comparer le nombre de requêtes SQL déclenchées (doit rester identique) et le pic mémoire

Ce scénario simule le cas typique d'une liste paginée où seules quelques lignes nécessitent le chargement de la relation.


Conclusion : lancer le POC maintenant

La conférence de Benjamin Eberlei aura lieu le 23 avril. D'ici là, on peut déjà avoir des chiffres.

Nous allons démarrer ce POC sur un projet Symfony 7 / Doctrine 3 en PHP 8.4 d'ici la fin du mois et publier les résultats ici. Si vous souhaitez qu'on l'applique à votre application — notamment pour quantifier l'impact sur vos temps de réponse ou votre consommation mémoire — contactez-nous.

Les lazy objects natifs ne sont pas une optimisation marginale : si les benchmarks confirment les promesses, c'est l'architecture proxy de Doctrine qui change de fond en comble. Autant être prêt.

Partager cet article