Aller au contenu principal
Retour au blog

PHP 8.4 : lazy objects, Dom\HTMLDocument, array_find et BcMath\Number

Flavien Métivier19 novembre 20245 min

PHP 8.4.0 sort officiellement le 21 novembre 2024 — dans deux jours à peine. Depuis le début des RC en septembre, la discussion technique a été largement dominée par les property hooks et l'asymmetric visibility, déjà traités dans un billet dédié. Ici, on s'attaque aux quatre autres additions structurantes de cette version : les lazy objects natifs, le nouveau parseur Dom\HTMLDocument conforme HTML5, les helpers array_find / array_any / array_all, et BcMath\Number pour les calculs à précision arbitraire. Aucune n'est anecdotique — chacune résout un problème de fond que tu contournais depuis des années avec des bibliothèques tierces ou des idiomes verbeux.

Lazy objects natifs — adieu ocramius/proxy-manager

Jusqu'ici, le lazy loading d'objets en PHP passait obligatoirement par des bibliothèques externes : ocramius/proxy-manager pour Symfony DI et Doctrine, ou des implémentations maison coûteuses à maintenir. PHP 8.4 intègre deux méthodes directement sur ReflectionClass : newLazyGhost() et newLazyProxy(). Le ghost initialise l'objet sur lui-même — même identité d'instance, zéro intermédiaire — tandis que le proxy délègue à un objet cible instancié uniquement à la première utilisation réelle. Symfony DI 7.x exploite déjà cette mécanique nativement ; sur PHP 8.4, la couche proxy-manager devient superflue dans la majorité des cas.

<?php

// PHP 8.4 — lazy ghost : __construct() n'est appelé qu'au premier accès
$reflector = new ReflectionClass(ProductRepository::class);

$repo = $reflector->newLazyGhost(function (ProductRepository $object): void {
    // Déclenchée uniquement à la première lecture/écriture d'une propriété
    $object->__construct(
        entityManager: $container->get(EntityManagerInterface::class)
    );
});

// $repo est bien une instance de ProductRepository
// mais __construct() n'a pas encore tourné — coût d'initialisation différé

Dom\HTMLDocument — le parseur HTML5 qu'on attendait depuis 15 ans

Le parseur HTML de DOMDocument repose sur libxml et devient imprévisible sur du HTML5 réel : balises void non fermées, doctype HTML5, attributs booléens... Le résultat était souvent un tas de warnings supprimés à coups d'opérateur @. Dom\HTMLDocument implémente le standard WHATWG HTML Living Standard — le même algorithme de parsing que les navigateurs modernes. Le nouveau namespace Dom\ expose également HTMLElement, HTMLCollection et les sélecteurs CSS via querySelector / querySelectorAll. L'ancienne classe \DOMDocument reste disponible sans modification : la migration peut se faire progressivement, fichier par fichier.

<?php

// Ancien comportement : warnings libxml sur du HTML5 réel, parsing approximatif
$old = new DOMDocument();
@$old->loadHTML('<article><p>Hello <b>world</b><br>Bonne lecture</p></article>');

// PHP 8.4 : Dom\HTMLDocument conforme WHATWG — aucun warning, résultat prédictible
$doc = Dom\HTMLDocument::createFromString(
    '<article><p>Hello <b>world</b><br>Bonne lecture</p></article>'
);

// querySelector / querySelectorAll natifs
$paragraphs = $doc->querySelectorAll('article p');
foreach ($paragraphs as $p) {
    echo $p->textContent . PHP_EOL; // "Hello worldBonne lecture"
}

// La rétro-compatibilité est préservée : \DOMDocument reste disponible sans modification

array_find, array_any, array_all : les helpers qu'on attendait

Quatre fonctions qui éliminent les idiomes verbeux à base de array_filter + reset() ou current(). Concises, lisibles, alignées sur les helpers natifs d'autres langages (firstOrDefault en C#, find en JavaScript). array_find() retourne le premier élément satisfaisant le prédicat, array_find_key() son index, array_any() et array_all() complètent l'ensemble pour les vérifications booléennes. Le tout sans aucune dépendance — et sans le risque d'erreur silencieuse que produisait current(array_filter(...)) sur un tableau vide.

Migration Symfony en vue ?

Estimer ma migration
<?php

$orders = [
    ['id' => 42, 'status' => 'pending', 'total' => '150.00'],
    ['id' => 43, 'status' => 'paid',    'total' => '89.50'],
    ['id' => 44, 'status' => 'paid',    'total' => '210.00'],
];

// Avant PHP 8.4 — verbeux et error-prone
$paid = current(array_filter($orders, fn($o) => $o['status'] === 'paid')) ?: null;

// PHP 8.4 — clair, immédiat
$firstPaid  = array_find($orders, fn($o) => $o['status'] === 'paid');     // ['id'=>43,...]
$firstKey   = array_find_key($orders, fn($o) => $o['status'] === 'paid'); // 1
$hasPending = array_any($orders, fn($o) => $o['status'] === 'pending');   // true
$allPaid    = array_all($orders, fn($o) => $o['status'] === 'paid');      // false

BcMath\Number — des opérateurs surchargés pour tes calculs financiers

Les fonctions procédurales bcadd(), bcsub(), bcmul() existent depuis PHP 4 et fonctionnent parfaitement — mais enchaîner plusieurs opérations les rend rapidement illisibles. BcMath\Number est un objet immutable qui surcharge les opérateurs arithmétiques (+, -, *, /, %, **) et de comparaison (<, >, ==…) via un mécanisme interne propre à la classe. Résultat : le code financier gagne en lisibilité sans rien sacrifier à la précision arbitraire de bcmath. Et comme la bibliothèque procédurale reste intacte, BcMath\Number est un ajout purement additif — rien ne casse.

<?php

use BcMath\Number;

$ht      = new Number('199.90');
$tauxTva = new Number('0.20');

// Opérateurs surchargés — précision arbitraire, aucune dérive flottante
$ttc = $ht + $ht * $tauxTva;
echo $ttc;            // 239.880
echo $ttc->round(2);  // 239.88

// Comparaisons directes
$seuil = new Number('200.00');
if ($ttc > $seuil) {
    echo 'Livraison offerte';
}

// Les fonctions procédurales bcmul(), bcadd() etc. restent disponibles
// BcMath\Number est additif, pas un remplacement cassant

Checklist de migration Symfony / Doctrine

  • Mettre à jour le Dockerfile : FROM php:8.4-fpm-alpine (image officielle disponible dès le 21/11)
  • Vérifier que ext-bcmath est activé dans l'image Docker — requis pour BcMath\Number
  • Passer "php": "^8.4" dans composer.json puis lancer composer update pour résoudre les conflits
  • Supprimer ocramius/proxy-manager s'il est installé uniquement pour le lazy loading (Symfony DI 7.x n'en a plus besoin sur PHP 8.4)
  • Mettre à jour PHPStan (^1.12+) et ses extensions pour les stubs PHP 8.4 — les lazy objects, Dom\ et BcMath\Number sont déjà couverts
  • Scanner les polyfills array_find maison à supprimer : un grep 'function array_find' dans src/ suffit
  • Auditer l'usage de DOMDocument avec l'opérateur @ : planifier la migration progressive vers Dom\HTMLDocument
  • Vérifier la compatibilité Doctrine ORM : les lazy proxies Doctrine restent fonctionnels, aucun changement requis côté attributs/annotations
  • Lancer la suite de tests sur les calculs financiers si tu utilises bcmath en mode procédural — BcMath\Number est additif, rien ne casse

PHP 8.4 n'est pas une mise à jour cosmétique. Les lazy objects natifs vont alléger les dépendances DI de nombreux projets Symfony, BcMath\Number va simplifier des pans entiers de code financier qui traînent en production depuis des années, et Dom\HTMLDocument ouvre enfin la voie à un parsing HTML fiable sans bidouilles libxml. Si ta stack tourne encore en 8.2 ou 8.3 et que tu veux préparer la migration sans régression, le Bear Upgrade est conçu pour ça : audit de compatibilité, plan de migration priorisé et accompagnement technique sur les breaking changes réels.

Cet article vous a plu ? Partagez-le !

Migration Symfony en vue ?

Bear Upgrade migre votre codebase 3-5× moins cher qu'une ESN.