Image de couverture : DTOs PHP compilés : comment passer de la réflexion à 4,5 millions d'hydrations par seconde
PHP & Frameworks

DTOs PHP compilés : comment passer de la réflexion à 4,5 millions d'hydrations par seconde

15 juillet 2026
5 min de lecture
7 vues
Sébastien Muler

Et si votre couche de données ralentissait votre application sans que vous le sachiez ?

Dans tout projet PHP sérieux, on finit par construire une couche d'objets de données : des classes typées, immuables, qui font le pont entre les entrées brutes (requêtes HTTP, lignes SQL, payloads d'API) et la logique métier. Les bibliothèques de DTOs pilotées par attributs ont rendu cette pratique agréable à écrire — mais agréable à écrire ne signifie pas forcément efficace à exécuter.

C'est précisément ce problème que Yurii Zub adresse dans son article publié sur dev.to, en décrivant l'architecture de Simple Data Objects, une bibliothèque qui remplace la réflexion PHP par de la compilation de closures. Résultat : 4,5 millions d'hydrations par seconde. Voici les mécanismes clés, traduits dans une perspective opérationnelle.

Le coût caché de la réflexion PHP

Prenons un DTO classique avec des attributs de validation et de cast :

class CreateOrderData extends BaseData {
    public function __construct(
        #[Rules(['required', 'string', 'max:200'])]
        public readonly string $title,
        #[Cast(new DateTimeCast('Y-m-d'))]
        public readonly \DateTime $deliveryDate,
        public readonly ?string $notes = null,
    ) {}
}

$order = CreateOrderData::from($request->all());

L'implémentation naïve derrière ce ::from() fait, à chaque appel :

  1. Un ReflectionClass sur le constructeur
  2. Une lecture des attributs pour chaque paramètre
  3. Un pipeline d'hydratation générique qui dispatche les casts et validations

Pour un seul DTO par requête, c'est acceptable. Mais dès qu'on hydrate des milliers de lignes depuis une requête SQL de rapport, ou qu'on sérialise des graphes d'objets imbriqués à chaque réponse d'API, ce coût se paie très cher — et il se paie à chaque appel.

La compilation de closures : le principe fondamental

L'approche compilée inverse la logique : plutôt que d'interpréter les métadonnées du DTO au moment de l'exécution, on génère une closure spécialisée une seule fois par classe, puis on la réutilise indéfiniment.

Concrètement, pour une propriété simple comme public readonly string $title, le générateur produit du code PHP équivalent à :

$closure = static function(array $data): CreateOrderData {
    $instance = new CreateOrderData(
        title: $data['title'],
        deliveryDate: $data['deliveryDate'],
        notes: $data['notes'] ?? null,
    );
    return $instance;
};

Pas de réflexion. Pas de dispatch générique. De la lecture de tableau inline, directement compilée par l'interpréteur PHP en opcodes optimisés.

Persistance via l'opcache

Ce qui rend cette approche vraiment efficace, c'est que les closures générées sont sérialisées dans des fichiers PHP qui sont ensuite servis par l'opcache. Dès le second appel — et pour tous les appels suivants jusqu'au prochain déploiement — PHP charge la closure depuis l'opcache sans aucun travail de compilation ni de réflexion. Le gain de performance est structurel, pas conjoncturel.

PHP 8.4 et les lazy ghosts

L'article introduit également l'utilisation des lazy ghosts de PHP 8.4. Quand un objet est marqué comme lazy, PHP en crée une coquille vide sans exécuter son constructeur. L'hydratation réelle n'intervient qu'au premier accès à une propriété.

Dans les cas où on charge une collection d'objets mais n'en consulte qu'une fraction (filtrage post-requête, pagination partielle, etc.), cette mécanique permet d'éviter l'hydratation des objets jamais consultés. Le gain est proportionnel à la densité des accès : dans les pires cas (tout est lu), l'impact est neutre ; dans les meilleurs cas, il est très significatif.

Les chiffres, étape par étape

L'auteur documente les gains mesurés à chaque étape de l'optimisation :

Approche Hydrations/s
Réflexion pure à chaque appel ~200 000
Mise en cache en mémoire (sans opcache) ~900 000
Closures compilées + opcache ~3 200 000
Closures compilées + opcache + PHP 8.4 lazy ghosts ~4 500 000

Ces chiffres ne sont pas des optimisations marginales : on parle d'un facteur 22x entre le point de départ et le résultat final. Ce type d'écart change fondamentalement ce que vous pouvez faire avec une même infrastructure.

Ce que cela signifie concrètement

Pour les projets PHP d'envergure — notamment ceux basés sur Symfony ou Laravel — cette architecture de DTO compilé a des implications directes :

  • Les endpoints de rapport lourd qui hydratent des milliers de lignes peuvent tenir sans cache applicatif supplémentaire
  • La sérialisation de graphes d'objets imbriqués dans les APIs REST ou GraphQL devient moins problématique à scaler
  • La montée en charge se fait sans augmenter linéairement le coût CPU lié à l'hydratation des données

L'approche est également transparente pour le développeur : le code d'écriture des DTOs reste identique. C'est la couche d'infrastructure qui change, pas l'API publique.

Conclusion

La réflexion PHP est un outil puissant, mais c'est un outil de découverte — pas d'exécution répétée. Compiler une seule fois ce qui peut l'être, persister le résultat dans l'opcache, et exploiter les primitives lazy de PHP 8.4 : c'est une combinaison qui transforme une contrainte de performance en avantage structurel.

Si votre application manipule des volumes significatifs de DTOs — et dans tout projet PHP mature, c'est inévitable — il vaut la peine d'examiner si votre implémentation actuelle compile ou interprète. La différence peut se chiffrer en millions d'opérations par seconde.

📄 Article original : Compiling PHP DTOs: from reflection to 4.5M hydrations per second par Yurii Zub sur dev.to

Partager cet article