PHP 8.4 est sorti le 21 novembre. Les property hooks sont la fonctionnalité. Tout le reste, et il y en a beaucoup, est secondaire.
Les property hooks
Pendant vingt ans, si on voulait du comportement à l’accès d’une propriété en PHP, il fallait écrire des getters et setters :
class User {
private string $_name;
public function getName(): string { return $this->_name; }
public function setName(string $name): void {
$this->_name = strtoupper($name);
}
}
PHP 8.4 ajoute des hooks directement sur la propriété :
class User {
public string $name {
set(string $name) {
$this->name = strtoupper($name);
}
}
}
On peut définir les hooks get et set indépendamment. Une propriété avec seulement un hook get est calculée à l’accès :
class Circle {
public float $area {
get => M_PI * $this->radius ** 2;
}
public function __construct(public float $radius) {}
}
Pas de stockage backing, pas de méthode getter explicite, support IDE complet. Les interfaces peuvent aussi déclarer des propriétés avec des hooks, ce qui signifie que les contrats peuvent maintenant spécifier un comportement à l’accès aux propriétés, quelque chose qui était tout simplement impossible avant.
La visibilité asymétrique
Une option plus légère pour quand on veut juste une lecture publique et une écriture privée :
class Version {
public private(set) string $value = '1.0.0';
}
$v = new Version();
echo $v->value; // fonctionne
$v->value = '2.0'; // Error
Élimine le pattern private $x + public getX() pour les propriétés publiques en lecture seule sans avoir besoin de la sémantique readonly complète.
array_find() et amis
$first = array_find($users, fn($u) => $u->isActive());
$any = array_any($users, fn($u) => $u->isPremium());
$all = array_all($users, fn($u) => $u->isVerified());
Ces fonctions existent dans la bibliothèque standard de chaque autre langage depuis des décennies. En PHP, il fallait utiliser array_filter() + accès par index ou écrire une boucle manuelle. Elles existent maintenant : array_find(), array_find_key(), array_any(), array_all().
Instanciation sans parenthèses supplémentaires
// avant
(new MyClass())->method();
// après
new MyClass()->method();
Une restriction syntaxique qui était toujours agaçante et jamais justifiée est supprimée.
Les objets paresseux
Des objets dont l’initialisation est différée jusqu’au premier accès à une propriété :
$user = $reflector->newLazyProxy(fn() => $repository->find($id));
// Pas d'appel en base encore
$user->name; // Maintenant le proxy s'initialise
Le public direct est les auteurs de frameworks ORM et de conteneurs DI, pas les développeurs d’applications. Mais l’effet se fait sentir dans chaque application qui utilise Doctrine ou Symfony : le lazy loading implémenté au niveau du langage plutôt qu’à travers la génération de code.
PHP 8.4 est un langage qui ressemble à peine au PHP 5 avec lequel la plupart d’entre nous avons commencé. Les property hooks en particulier : ce ne sont pas des contournements, ce sont une fonctionnalité de conception.
#[\Deprecated] pour son propre code
PHP émet des notices de dépréciation pour les fonctions intégrées depuis des années. 8.4 permet de câbler le même mécanisme dans son propre code :
class ApiClient {
#[\Deprecated(
message: 'Use fetchJson() instead',
since: '2.0',
)]
public function get(string $url): string { ... }
}
Appeler une méthode dépréciée émet maintenant E_USER_DEPRECATED, exactement comme appeler mysql_connect(). Les IDEs le détectent, les analyseurs statiques le signalent, le log d’erreurs le capture. Avant ça, la seule option était un commentaire PHPDoc @deprecated : bien pour les IDEs, complètement invisible pour le moteur.
BcMath\Number rend la précision arbitraire utilisable
Les fonctions bcmath existent en PHP depuis toujours, mais leur API procédurale rend tout chaînage pénible. 8.4 ajoute BcMath\Number, un wrapper objet avec surcharge d’opérateurs :
$a = new BcMath\Number('10.5');
$b = new BcMath\Number('3.2');
$result = $a + $b; // BcMath\Number('13.7')
$result = $a * $b - new BcMath\Number('1');
echo $result; // 32.6
Les opérateurs +, -, *, /, **, % fonctionnent tous. L’objet est immutable. L’échelle se propage automatiquement à travers les opérations. Les calculs financiers, qui nécessitaient des chaînes de bcadd(bcmul(...), ...), se lisent maintenant comme de l’arithmétique.
De nouvelles fonctions procédurales complètent le tableau : bcceil(), bcfloor(), bcround(), bcdivmod().
L’enum RoundingMode remplace les constantes PHP_ROUND_*
round() a toujours pris un $mode entier depuis un ensemble de constantes PHP_ROUND_*. 8.4 les remplace par un enum RoundingMode avec des noms plus propres et quatre modes supplémentaires qui n’étaient pas disponibles avant :
round(2.5, mode: RoundingMode::HalfAwayFromZero); // 3
round(2.5, mode: RoundingMode::HalfTowardsZero); // 2
round(2.5, mode: RoundingMode::HalfEven); // 2 (arrondi du banquier)
round(2.5, mode: RoundingMode::HalfOdd); // 3
// Les quatre nouveaux modes (disponibles uniquement via l'enum)
round(2.3, mode: RoundingMode::TowardsZero); // 2
round(2.7, mode: RoundingMode::AwayFromZero); // 3
round(2.3, mode: RoundingMode::PositiveInfinity); // 3
round(2.3, mode: RoundingMode::NegativeInfinity); // 2
Les anciennes constantes PHP_ROUND_* fonctionnent encore. L’enum est la voie à suivre.
Les fonctions de string multibyte qui auraient dû exister
mb_trim(), mb_ltrim(), mb_rtrim() : des fonctions de trim qui respectent les frontières de caractères multibyte, pas juste les espaces ASCII. Aussi nouvelles : mb_ucfirst() et mb_lcfirst() pour la mise en majuscule correcte des strings multibyte.
$s = "\u{200B}hello\u{200B}"; // Espaces de largeur nulle
echo mb_trim($s); // "hello"
echo mb_ucfirst('über'); // "Über"
Ces fonctions comblent des lacunes présentes depuis que mbstring a été introduit.
request_parse_body() pour les requêtes non-POST
PHP parse automatiquement application/x-www-form-urlencoded et multipart/form-data dans $_POST et $_FILES, mais seulement pour les requêtes POST. Les requêtes PATCH et PUT avec les mêmes types de contenu nécessitaient un parsing manuel avec file_get_contents('php://input') et du code personnalisé.
// Dans un handler PATCH
[$_POST, $_FILES] = request_parse_body();
La fonction retourne un tuple. Même logique de parsing que PHP utilise pour POST, maintenant disponible pour n’importe quelle méthode HTTP.
Une nouvelle API DOM qui suit la spec
L’API DOMDocument existante était construite sur une spec DOM level 3 plus ancienne avec des spécificités PHP superposées. 8.4 ajoute un namespace Dom\ parallèle qui implémente le WHATWG Living Standard :
$doc = Dom\HTMLDocument::createFromString('<p class="lead">Hello</p>');
$p = $doc->querySelector('p');
echo $p->classList; // "lead"
echo $p->id; // ""
$doc2 = Dom\HTMLDocument::createFromFile('page.html');
Dom\HTMLDocument parse correctement HTML5, tag soup inclus. Dom\XMLDocument gère le XML strict. Les nouvelles classes sont strictes sur les types, retournent les bons types de nœuds, et exposent des propriétés modernes comme classList, id, className. L’ancien DOMDocument reste, inchangé, pour la compatibilité ascendante.
PDO reçoit des sous-classes spécifiques au driver
PDO::connect() et l’instanciation directe retournent maintenant des sous-classes spécifiques au driver quand elles sont disponibles :
$pdo = PDO::connect('mysql:host=localhost;dbname=test', 'user', 'pass');
// $pdo est maintenant une instance Pdo\Mysql
$pdo = new Pdo\Pgsql('pgsql:host=localhost;dbname=test', 'user', 'pass');
Chaque sous-classe driver (Pdo\Mysql, Pdo\Pgsql, Pdo\Sqlite, Pdo\Firebird, Pdo\Odbc, Pdo\DbLib) peut exposer des méthodes spécifiques au driver sans polluer l’interface PDO de base. Doctrine, Laravel et autres ORMs similaires peuvent maintenant type-hinter contre la classe de driver spécifique quand ils ont besoin d’un comportement spécifique au driver.
OpenSSL reçoit le support des clés modernes
openssl_pkey_new() et les fonctions associées supportent maintenant Curve25519 et Curve448, les courbes elliptiques modernes qui ont remplacé les anciennes courbes NIST dans la plupart des recommandations de sécurité :
$key = openssl_pkey_new(['curve_name' => 'ed25519', 'private_key_type' => OPENSSL_KEYTYPE_EC]);
$details = openssl_pkey_get_details($key);
x25519 et x448 pour l’échange de clés, ed25519 et ed448 pour les signatures. Les quatre fonctionnent maintenant avec openssl_sign() et openssl_verify().
PCRE : lookbehind de longueur variable
La mise à jour de la bibliothèque PCRE2 embarquée (10.44) apporte les assertions lookbehind de longueur variable, quelque chose que les moteurs regex de Perl et Python avaient et que PHP ne pouvait pas faire :
// Correspondre "bar" seulement quand précédé de "foo" ou "foooo"
preg_match('/(?<=foo+)bar/', 'foooobar', $matches);
Les assertions lookbehind nécessitaient auparavant un pattern de largeur fixe. Elles peuvent maintenant correspondre à des patterns de longueur variable. Le modificateur r (PCRE2_EXTRA_CASELESS_RESTRICT) est aussi nouveau : il empêche le mélange de caractères ASCII et non-ASCII dans les correspondances insensibles à la casse, fermant une classe d’attaques de confusion Unicode.
DateTime reçoit les microsecondes et une factory de timestamp
$dt = DateTimeImmutable::createFromTimestamp(1700000000.5);
echo $dt->getMicrosecond(); // 500000
$with_micros = $dt->setMicrosecond(123456);
createFromTimestamp() accepte un float pour une précision sous-seconde. getMicrosecond() et setMicrosecond() complètent l’API pour le composant microseconde qui était à l’intérieur de DateTime mais inaccessible directement.
fpow() pour la conformité IEEE 754
pow(0, -2) en PHP retournait historiquement une valeur définie par l’implémentation. 8.4 déprécie pow() avec une base zéro et un exposant négatif et introduit fpow(), qui suit strictement IEEE 754 : fpow(0, -2) retourne INF, comme le standard le définit :
echo fpow(2.0, 3.0); // 8.0
echo fpow(0.0, -1.0); // INF
echo fpow(-1.0, INF); // 1.0
À retenir dans tout code faisant des calculs mathématiques où la conformité IEEE compte.
Le coût de bcrypt augmente
Le coût par défaut pour password_hash() avec PASSWORD_BCRYPT est passé de 10 à 12. Ça impacte tout code appelant password_hash($pass, PASSWORD_BCRYPT) sans coût explicite. L’objectif est de maintenir le défaut grossièrement à “quelques centaines de millisecondes sur du matériel moderne” à mesure que le matériel devient plus rapide.
Si on stocke des hash bcrypt et qu’on monte sur 8.4, les hash existants restent valides : password_verify() lit le coût depuis le hash lui-même. Les nouveaux hash utilisent le coût 12. password_needs_rehash() retourne true pour les anciens hash si on passe ['cost' => 12], donc on peut les mettre à jour à la prochaine connexion.
Les dépréciations qui comptent
Les paramètres implicitement nullable sont dépréciés. Si un paramètre a un défaut de null, le type doit le dire explicitement :
// Déprécié dans 8.4
function foo(string $s = null) {}
// Correct
function foo(?string $s = null) {}
function foo(string|null $s = null) {}
trigger_error() avec E_USER_ERROR est déprécié : le remplacer par une exception ou exit(). Le niveau E_USER_ERROR a toujours été un hybride maladroit entre une erreur récupérable et une erreur fatale, et personne n’était sûr lequel.
lcg_value() est aussi déprécié. Utiliser Random\Randomizer::getFloat() à la place. Le générateur LCG avait de mauvaises propriétés d’aléatoire et aucun contrôle de graine.