PHP 8.1 est sorti le 25 novembre. Il fait suite à la refonte massive de 8.0 avec quelque chose de différent : moins de fonctionnalités, mais chacune vraiment réfléchie plutôt que greffée à la va-vite.
Les enums
C’est la nouveauté qui change les bases de code dès la mise à jour. Avant 8.1, les énumérations en PHP se résumaient à des constantes de classe, des chaînes ou des entiers sans rien pour les faire respecter :
// avant : rien n'empêche de passer Status::INVALID
const ACTIVE = 'active';
const INACTIVE = 'inactive';
// après
enum Status: string {
case Active = 'active';
case Inactive = 'inactive';
}
function activate(Status $status): void { ... }
Les enums PHP sont des objets, pas des scalaires. Ils supportent les méthodes, les interfaces et les constantes. Les backed enums (avec une valeur string ou int) se sérialisent proprement et se mappent naturellement aux colonnes de base de données. Les pure enums (sans type de backing) expriment des concepts métier sans se soucier de la sérialisation.
L’effet immédiat : chaque champ de statut, chaque ensemble fini d’états dans toutes les bases de code que je maintiens est devenu un candidat à l’enum. Le système de types a enfin un moyen natif d’exprimer ce que chaque projet PHP simulait depuis des années.
Les fibers
Les fibers sont une primitive de concurrence coopérative : vous pouvez suspendre et reprendre l’exécution d’une fonction, en cédant le contrôle sans recourir aux threads.
$fiber = new Fiber(function(): void {
$value = Fiber::suspend('first');
echo "Repris avec : {$value}\n";
});
$result = $fiber->start(); // 'first'
$fiber->resume('hello'); // "Repris avec : hello"
Les fibers sont la fondation dont les bibliothèques async comme ReactPHP et Amp avaient besoin depuis un moment du côté du runtime. Pour la plupart des développeurs d’applications, l’API directe compte moins que les bibliothèques construites par-dessus, mais comprendre les fibers explique ce que font ces bibliothèques en coulisses.
:pencil2: Les propriétés readonly
8.0 avait apporté la promotion des paramètres du constructeur. 8.1 ajoute readonly :
class User {
public function __construct(
public readonly int $id,
public readonly string $name,
) {}
}
Une propriété readonly ne peut être écrite qu’une seule fois, lors de l’initialisation. Après ça, toute écriture lève une Error. Combiné avec la promotion des paramètres, les value objects et les DTOs deviennent concis et signifient réellement ce qu’ils annoncent.
La syntaxe callable de première classe
$fn = strlen(...);
$fn = $this->process(...);
$fn = MyClass::create(...);
... après un callable crée une Closure sans le boilerplate de Closure::fromCallable(). Utile quand on passe des méthodes comme callbacks.
8.1 est précis. Les enums justifient à eux seuls la mise à jour.
Les types d’intersection
Les types union ont débarqué en 8.0. Les types d’intersection suivent en 8.1. Là où un union dit « l’un ou l’autre », une intersection dit « tous à la fois » :
function process(Countable&Iterator $collection): void {
foreach ($collection as $item) { /* ... */ }
echo count($collection);
}
Une contrainte : les types d’intersection ne peuvent pas être mélangés avec les types union dans la même déclaration (ça arrivera en 8.2 avec les DNF types). Mais ça débloque déjà une vérification de types précise pour les objets qui doivent satisfaire plusieurs interfaces à la fois, un pattern que les frameworks utilisent constamment et qui devait rester sans typage jusqu’ici.
Le type de retour never
Une fonction qui ne retourne jamais (elle lève toujours une exception ou sort) a maintenant un type pour le dire :
function redirect(string $url): never {
header("Location: {$url}");
exit();
}
function fail(string $message): never {
throw new \RuntimeException($message);
}
L’avantage concret : les analyseurs statiques peuvent prouver que le code après une fonction never est inatteignable, et les appelants savent qu’il n’y a pas de valeur de retour à gérer. Avant ça, ça vivait dans des docblocks sans enforcement.
Les constantes de classe finales
Avant 8.1, n’importe quelle sous-classe pouvait silencieusement surcharger la constante de classe d’un parent. Maintenant vous pouvez y mettre un terme :
class Base {
final public const VERSION = '1.0';
}
class Child extends Base {
// Fatal error: Cannot override final constant Base::VERSION
public const VERSION = '2.0';
}
Parallèlement, les constantes d’interface sont désormais surchargeables par les classes implémentant l’interface par défaut. Un correctif de comportement qui était incohérent depuis l’introduction des interfaces.
new dans les initialiseurs
Les valeurs par défaut des paramètres étaient autrefois limitées aux scalaires et aux tableaux. 8.1 lève cette restriction :
class Logger {
public function __construct(
private Handler $handler = new NullHandler(),
) {}
}
function createUser(
Validator $validator = new DefaultValidator(),
): User { /* ... */ }
Idem pour les arguments d’attributs et les initialiseurs de variables statiques. Ce qui signifie que l’injection de dépendances avec des valeurs par défaut sensées ne nécessite plus une vérification de null et une instanciation paresseuse dans le corps de la méthode.
Le déballage de tableaux avec des clés string
Le déballage de tableau via l’opérateur spread ne fonctionnait qu’avec des tableaux à clés entières avant 8.1. Les clés string fonctionnent aussi maintenant :
$defaults = ['color' => 'red', 'size' => 'M'];
$custom = ['size' => 'L', 'weight' => '200g'];
$merged = [...$defaults, ...$custom];
// ['color' => 'red', 'size' => 'L', 'weight' => '200g']
Les clés ultérieures écrasent les précédentes. Même comportement que array_merge(), mais exprimé inline. La différence de performance est marginale ; la différence de lisibilité, elle, ne l’est pas.
fsync et fdatasync
Deux fonctions qui n’avaient aucune bonne raison d’être absentes d’un langage orienté système de fichiers :
$fp = fopen('/tmp/important.dat', 'w');
fwrite($fp, $data);
fsync($fp); // vide les buffers OS vers le stockage physique
fclose($fp);
fdatasync() fait la même chose mais saute la synchronisation des métadonnées quand on ne se soucie que de la durabilité des données. Les deux retournent false en cas d’échec. Si vous écrivez quoi que ce soit qui nécessite une sécurité en cas de crash, vous aviez besoin de ça.
Passer null aux paramètres non-nullables des fonctions internes
Un changement plus discret mais aux conséquences réelles : les fonctions internes qui acceptent des chaînes, des entiers, etc. ont toujours avalé silencieusement null et l’ont coercé. En 8.1, ça commence à émettre un avertissement de dépréciation.
str_contains("foobar", null);
// Deprecated: Passing null to parameter #2 ($needle) of type string is deprecated
Ça aligne les fonctions internes sur les fonctions définies par l’utilisateur, qui refusaient déjà les arguments nullable pour des paramètres non-nullables. PHP 9.0 transforme ça en erreur fatale. Si vous passez null dans des fonctions de chaînes, c’est maintenant un meilleur moment pour le corriger que pendant un incident de production.
MySQLi lève des exceptions par défaut
Avant 8.1, MySQLi échouait silencieusement sauf si vous appeliez explicitement mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT). C’est maintenant la valeur par défaut :
// Ceci lève \mysqli_sql_exception en cas d'échec de connexion en 8.1
// Auparavant retournait false et définissait une erreur que vous deviez vérifier manuellement
$connection = new mysqli('localhost', 'user', 'wrong_password', 'db');
Toute base de code qui attrape les erreurs MySQLi en vérifiant les valeurs de retour doit être revue. Les échecs silencieux qui causaient des bugs difficiles à diagnostiquer lèvent maintenant des exceptions, ce qui est le bon comportement, même si ça peut surprendre si vous l’attrapez en pleine mise à jour.