PHP 7.4 est sorti le 28 novembre. C’est la dernière version 7.x avant PHP 8.0, et ça se sent. Les fonctionnalités sont suffisamment substantielles pour tenir debout seules, mais elles ressemblent aussi à des fondations pour ce qui arrive.

Les propriétés typées

C’est la grande nouveauté. Depuis PHP 7.0, on pouvait typer les paramètres de fonctions et les valeurs de retour. Mais les propriétés de classe ? Toujours non typées :

class User {
    public int $id;
    public string $name;
    public ?DateTimeInterface $deletedAt;
}

7.4 change ça. Les propriétés typées font respecter les types à l’affectation, pas seulement au niveau des sites d’appel. Les classes deviennent auto-documentées d’une façon que les docblocks n’ont jamais vraiment réussi à faire, et le moteur attrape les erreurs de type avant qu’elles ne se propagent dans la moitié de la stack.

Une subtilité : les propriétés typées sont uninitialized par défaut (pas null). Accéder à une propriété non initialisée lève une Error. C’est un piège classique : ?string n’implique pas un défaut de null. Il faut encore un = null explicite pour ça.

Les arrow functions

Les closures en PHP ont toujours nécessité d’importer explicitement les variables de la portée extérieure avec use :

$multiplier = 3;
$fn = fn($x) => $x * $multiplier; // pas besoin de use()

Les arrow functions capturent automatiquement la portée englobante. Expression unique, retour implicite, pas de boilerplate. Elles ne remplacent pas les closures complètes pour la logique complexe, mais pour les callbacks courts, elles éliminent une catégorie de bruit qui s’accumulait depuis des années.

Le préchargement opcache

Pour les setups PHP-FPM à longue durée de vie, le préchargement permet à un script de charger et compiler des fichiers PHP en mémoire opcache au démarrage du serveur. Ces fichiers sont disponibles pour toutes les requêtes sans overhead de compilation.

Le gain varie selon l’application. Sur les grands frameworks où les mêmes fichiers sont chargés à chaque requête, c’est réel. Sur les petites applications, négligeable. Vaut la peine de benchmarker avant d’ajouter la complexité de configuration.

Les petites choses qui s’accumulent

Les fonctionnalités mentionnées en passant méritent plus qu’une ligne. L’opérateur d’affectation null-coalescente ??= résout un pattern suffisamment agaçant à écrire à chaque fois, mais jamais assez pour se donner la peine de l’abstraire :

$config['timeout'] ??= 30;
// équivalent à : $config['timeout'] = $config['timeout'] ?? 30;

L’opérateur spread dans les littéraux de tableau fait ce qu’on attend de la version pour les appels de fonctions — dépacker un itérable dans un littéral de tableau :

$defaults = ['color' => 'blue', 'size' => 'M'];
$options = ['size' => 'L', ...$defaults, 'weight' => 1.2];
// ['size' => 'M', 'color' => 'blue', 'weight' => 1.2]

Note : les clés string n’étaient pas supportées dans 7.4 pour le dépaquet de tableau. Ça viendra plus tard.

Les types de retour covariants et les types de paramètres contravariants comblent un vide qui rendait certains patterns d’héritage inutilement maladroits. Une classe enfant peut maintenant affiner son type de retour vers un sous-type de celui du parent, sans erreur fatale :

class Producer {
    public function get(): Iterator {}
}
class ChildProducer extends Producer {
    public function get(): ArrayIterator {} // ArrayIterator implémente Iterator
}

Lire des nombres à 3h du matin

Le séparateur de littéraux numériques est une de ces fonctionnalités dont on ne sait pas qu’on la voulait jusqu’à la première fois qu’on écrit une grande constante et qu’on perd immédiatement le sens de l’ordre de grandeur :

$earthMass    = 5_972_168_000_000_000_000_000_000; // kg
$lightSpeed   = 299_792_458;                        // m/s
$planck       = 6.626_070_15e-34;                  // J·s
$hexMask      = 0xFF_EC_D5_08;
$binaryFlags  = 0b0001_1111_0010_0000;

L’underscore est purement syntaxique. Le moteur le supprime avant de parser la valeur. On peut le mettre n’importe où entre les chiffres, bien que la convention suive le groupement naturel du système numérique utilisé.

Référencer sans posséder

WeakReference permet de tenir une référence à un objet sans empêcher le ramasse-miettes de le détruire. Le cas d’usage : les caches et registres — on veut savoir qu’un objet est vivant, mais on ne veut pas être la raison qu’il reste vivant :

$object = new HeavyObject();
$ref = WeakReference::create($object);

var_dump($ref->get()); // object(HeavyObject)

unset($object);

var_dump($ref->get()); // NULL — le GC l'a collecté

Avant 7.4, il y avait WeakRef via une extension, et certains frameworks faisaient des tours de passe-passe avec SplObjectStorage qui ne se comportaient pas tout à fait pareil. La classe native est juste directe.

La sérialisation sans surprise

La sérialisation personnalisée d’objets avant 7.4 passait par l’interface Serializable : implémenter serialize() et unserialize(), retourner une string, reconstruire depuis elle. Le problème est que serialize() déclenchait __sleep(), unserialize() déclenchait __wakeup(), et l’interaction entre ces hooks était fragile, surtout dans les hiérarchies d’héritage.

7.4 introduit __serialize() et __unserialize(), qui travaillent avec des tableaux plutôt que des strings et n’interagissent pas avec les anciens hooks :

class Session {
    private string $token;
    private \DateTime $createdAt;

    public function __serialize(): array {
        return ['token' => $this->token, 'created' => $this->createdAt->getTimestamp()];
    }

    public function __unserialize(array $data): void {
        $this->token = $data['token'];
        $this->createdAt = (new \DateTime())->setTimestamp($data['created']);
    }
}

Quand les nouvelles et anciennes méthodes coexistent sur la même classe, __serialize() gagne. L’ancienne interface Serializable est dépréciée dans 8.1.

Ce que la bibliothèque standard a discrètement reçu

mb_str_split() fait ce que str_split() fait mais correctement pour les strings multibyte. Le manque était franchement embarrassant pour un langage utilisé dans autant de locales que PHP :

mb_str_split('héllo', 1); // ['h', 'é', 'l', 'l', 'o']
str_split('héllo', 1);    // ['h', 'Ã', '©', 'l', 'l', 'o'] — cassé

strip_tags() accepte maintenant un tableau de tags autorisés, ce qui est plus propre que le format string qu’il fallait passer auparavant :

strip_tags($html, ['p', 'br', 'strong']); // était : '<p><br><strong>'

proc_open() accepte maintenant un tableau de commandes, contournant complètement l’interprétation par le shell. Même idée que subprocess de Python avec shell=False. À retenir quand on passe des arguments fournis par l’utilisateur à un processus externe.

Le chapitre FFI

L’extension Foreign Function Interface a atterri dans 7.4 après avoir passé du temps dans une branche feature. Elle permet à PHP d’appeler des fonctions C natives en chargeant une bibliothèque partagée et en déclarant les signatures :

$ffi = FFI::cdef("int strlen(const char *s);", "libc.so.6");
var_dump($ffi->strlen("hello")); // int(5)

Les applications pratiques sont étroites mais réelles : appeler des API de plateforme sans binding PHP, wrapper du code C critique pour les performances sans écrire une extension complète, ou juste jouer avec des bibliothèques natives directement. Ce n’est pas un remplacement des extensions propres en production, mais ça supprime la barrière “écrire une extension C” pour l’exploration.

Ce qui a été déprécié

Quelques choses qui auraient dû être nettoyées depuis longtemps ont finalement reçu le traitement dépréciation dans 7.4.

Les ternaires imbriqués sans parenthèses ont toujours été ambigus. PHP les évaluait de gauche à droite alors que pratiquement tous les autres langages avec un ternaire évaluent de droite à gauche :

// Était ambigu, maintenant déprécié :
$a ? $b : $c ? $d : $e;

// Rendre explicite :
($a ? $b : $c) ? $d : $e;

L’accès par offset avec accolades pour les strings et tableaux — $str{0} au lieu de $str[0] — est déprécié et supprimé dans 8.0. C’était toujours un alias, jamais une fonctionnalité distincte.

implode() avec l’ordre d’arguments inversé (tableau en premier, colle en second) est déprécié. La fonction a accepté les deux ordres depuis le début, ce qui était une erreur. L’ordre correct est implode(string $separator, array $array).

Ce qui arrive ensuite

7.4 est la dernière version 7.x. Les dépréciations sont principalement du déblayage pour les suppressions dans 8.0. Le backlog de RFCs pour 8.0 est substantiel : JIT, attributs, arguments nommés, expressions match. 7.4 est un bon endroit où atterrir en attendant que tout ça arrive.