PHP 8.3 est sorti le 23 novembre. Une version discrète par les standards PHP : pas de bouleversement à la taille des enums, pas de JIT. Ce qu’elle apporte, c’est un ensemble ciblé d’améliorations qui comblent des lacunes de longue date dans le système de types et ajoutent des fonctions qui auraient dû exister depuis des années.
Les constantes de classe typées
Les constantes de classe n’ont jamais été typées depuis leur introduction. PHP 8.3 corrige ça :
interface HasVersion {
const string VERSION;
}
class App implements HasVersion {
const string VERSION = '1.0.0';
}
Sans constantes typées, une constante d’interface pouvait être redéfinie avec un type complètement différent dans une classe implémentante sans que rien ne se plaigne. Les constantes typées comblent ce trou, et sur les bases de code pilotées par les interfaces, l’impact est immédiat.
L’accès dynamique aux constantes de classe
Une lacune qui nécessitait un contournement depuis que les constantes existent :
$name = 'STATUS';
echo MyClass::{$name}; // ça marche maintenant
Avant, accéder à une constante avec un nom dynamique signifiait appeler constant('MyClass::STATUS'). La nouvelle syntaxe est cohérente avec la façon dont PHP gère déjà les variables variables et les appels de méthodes dynamiques.
readonly peut maintenant être modifié dans clone
Une limitation spécifique mais vraiment agaçante de 8.1 avec readonly : on ne pouvait pas cloner un objet et changer une propriété readonly. 8.3 ajoute la possibilité de réinitialiser les propriétés readonly pendant le clonage, ce qui rend les value objects immuables utilisables dans beaucoup plus de patterns.
json_validate()
if (json_validate($string)) {
$data = json_decode($string);
}
Avant 8.3, la seule façon de valider une chaîne JSON était de la décoder et de vérifier les erreurs. json_validate() vérifie sans allouer la structure décodée, ce qui compte quand on a juste besoin de savoir si la chaîne est du JSON valide, pas ce qu’elle contient.
Améliorations du Randomizer
getBytesFromString() génère une chaîne aléatoire composée uniquement de caractères d’un ensemble donné :
$rng = new Random\Randomizer();
$token = $rng->getBytesFromString('abcdefghijklmnopqrstuvwxyz0123456789', 32);
L’approche précédente : str_split, array_map, sélection aléatoire, implode. Ça marchait, mais c’était plus long que ça n’avait le droit d’être.
8.3 est pour les équipes qui adoptent les versions PHP rapidement et veulent les améliorations incrémentales. Les constantes typées seules valent le coup sur toute base de code avec des constantes d’interface.
#[\Override] rend l’héritage explicite
Avant 8.3, rien n’empêchait d’écrire une méthode qu’on croyait surcharger celle d’un parent, alors qu’on avait un typo dans le nom ou que le parent l’avait silencieusement supprimée. Des bugs silencieux, zéro retour du moteur.
class Cache {
#[\Override]
public function get(string $key): mixed {
// Le moteur vérifie que cette méthode existe dans un parent ou une interface
}
}
Si la méthode n’existe dans aucune classe parente ou interface implémentée, PHP lève une erreur. Même concept que @Override en Java ou override en C#, enfin en PHP.
final sur les méthodes de trait
Les traits ont toujours eu des aspérités dans le modèle OOP de PHP. Un problème spécifique : une classe utilisant un trait pouvait surcharger n’importe laquelle de ses méthodes, sapant les garanties que le trait essayait de fournir. 8.3 laisse le trait lui-même marquer une méthode comme final :
trait Singleton {
final public static function getInstance(): static {
// ...
}
}
Maintenant, une classe utilisant le trait ne peut pas surcharger getInstance(). La garantie tient.
Les classes anonymes peuvent être readonly
PHP 8.1 avait apporté les classes readonly. Les classes anonymes avaient été laissées de côté pour une raison obscure. 8.3 corrige ça :
$point = new readonly class(3, 4) {
public function __construct(
public float $x,
public float $y,
) {}
};
Pratique quand on a besoin d’un value object immuable jetable sans la cérémonie de lui donner un nom.
Les initialiseurs de variables statiques acceptent des expressions
Une restriction petite mais ancienne : les initialiseurs de variables statiques n’acceptaient que des expressions constantes, pas d’appels de fonctions. 8.3 lève cette contrainte :
function connection(): PDO {
static $pdo = new PDO(getenv('DATABASE_URL'));
return $pdo;
}
L’initialiseur s’exécute une seule fois au premier appel, la variable statique persiste. Faisable avec un test null avant, c’est juste plus propre.
mb_str_pad() existe enfin
str_pad() a toujours été conscient des octets, pas des caractères. Pour les chaînes multibyte (arabe, japonais, caractères accentués) il produisait une sortie incorrecte. 8.3 ajoute enfin la variante multibyte :
$padded = mb_str_pad('日本', 10, '*', STR_PAD_BOTH);
La fonction respecte les limites de caractères, pas les comptages d’octets.
str_increment() et str_decrement()
L’opérateur ++ de PHP sur les chaînes a une histoire de bizarreries : il incrémente les séquences de lettres ('a' → 'b', 'z' → 'aa'), mais -- n’a jamais fonctionné symétriquement. Le comportement était suffisamment surprenant que 8.3 déprécie ++/-- sur les chaînes non alphanumériques et introduit des fonctions explicites :
echo str_increment('a'); // b
echo str_increment('Az'); // Ba
echo str_decrement('b'); // a
echo str_decrement('Ba'); // Az
Les fonctions rendent l’intention évidente et le comportement prévisible.
Random\Randomizer gagne le support des flottants
8.3 comble le côté flottant de l’API Randomizer :
$rng = new Random\Randomizer();
// Un flottant dans [0.0, 1.0)
$f = $rng->nextFloat();
// Un flottant dans une plage spécifique avec contrôle de l'inclusion des bornes
$f = $rng->getFloat(1.5, 3.5, Random\IntervalBoundary::ClosedOpen);
IntervalBoundary est un nouvel enum avec quatre valeurs : ClosedOpen, ClosedClosed, OpenClosed, OpenOpen. C’est important pour la justesse statistique : l’approche naïve avec rand() / getrandmax() ne produit pas une distribution uniforme sur les flottants.
La hiérarchie d’exceptions de Date
Les erreurs de date/heure en PHP levaient des exceptions génériques sans moyen de distinguer “chaîne malformée” de “timezone invalide” sans parser le message. 8.3 ajoute une hiérarchie propre :
try {
new DateTimeImmutable('not a date');
} catch (DateMalformedStringException $e) {
// spécifiquement un échec de parsing
} catch (DateException $e) {
// autres erreurs liées aux dates
}
L’arbre complet : DateError (niveau moteur), DateException (base), avec des sous-classes spécifiques pour timezone invalide, chaîne d’intervalle malformée, chaîne de période malformée, et chaîne de date malformée.
gc_status() en dit plus
gc_status() retourne maintenant huit champs supplémentaires : running, protected, full, buffer_size, et des décompositions temporelles (application_time, collector_time, destructor_time, free_time). Si vous profilez la pression mémoire ou les pauses GC, ces données étaient auparavant inaccessibles sans passer par une extension.
strrchr() gagne un argument de direction
strrchr() (trouver la dernière occurrence d’un caractère, retourner de là jusqu’à la fin) accepte maintenant un booléen $before_needle, alignant son API sur celle de strstr() :
$path = '/var/www/html/index.php';
echo strrchr($path, '/', before_needle: true); // /var/www/html
echo strrchr($path, '/'); // /index.php
Une fonction présente dans PHP depuis 1994, enfin cohérente avec sa voisine.
Dépréciations à noter
get_class() et get_parent_class() sans arguments émettent maintenant des avertissements de dépréciation. Les formes sans argument reposaient sur un contexte $this implicite, facile à mal lire. Passez l’objet explicitement.
assert_options() et les constantes ASSERT_* sont dépréciées au profit de la directive INI zend.assertions, qui est le bon outil pour contrôler le comportement des assertions selon les environnements.
Les opérateurs ++/-- sur les chaînes vides et les chaînes non numériques non alphanumériques émettent maintenant des avertissements de dépréciation. Le comportement était un territoire indéfini. 8.3 amorce la migration vers un comportement défini en 9.0.
Protection contre les débordements de pile
Deux nouvelles directives INI : zend.max_allowed_stack_size fixe une limite dure sur la profondeur de pile de PHP, et zend.reserved_stack_size réserve un buffer pour le nettoyage après qu’une limite soit atteinte. Avant 8.3, un code récursif profond pouvait tout simplement crasher au niveau OS. Maintenant PHP l’intercepte et lève une Error avec un message utile.