PHP 8.5 est sorti le 20 novembre. Deux fonctionnalités définissent cette version : l’opérateur pipe et l’extension URI. Elles résolvent des problèmes différents, mais partagent la même motivation : rendre les opérations courantes moins maladroites à exprimer.

L’opérateur pipe

Les pipelines fonctionnels en PHP ont toujours été un bazar. Enchaîner des transformations nécessitait soit d’imbriquer les appels de fonctions à l’envers, soit de les découper en variables intermédiaires :

// avant — se lit de droite à gauche
$result = array_sum(array_map('strlen', array_filter($strings, 'strlen')));

// ou verbeux mais lisible
$filtered   = array_filter($strings, 'strlen');
$lengths    = array_map('strlen', $filtered);
$result     = array_sum($lengths);

// après — se lit de gauche à droite
$result = $strings
    |> array_filter(?, 'strlen')
    |> array_map('strlen', ?)
    |> array_sum(?);

L’opérateur |> passe la valeur de gauche dans l’expression de droite. Le placeholder ? marque où elle va. Les pipelines se lisent maintenant dans l’ordre où les opérations se produisent : gauche à droite, haut en bas.

Ça se marie bien avec les callables de première classe de PHP 8.1. Les deux fonctionnalités se composent bien :

$result = $input |> trim(...) |> strtolower(...) |> $this->normalize(...);

L’extension URI

Gérer les URIs en PHP a toujours signifié soit se tourner vers une bibliothèque tierce, soit assembler parse_url() (retourne un tableau, pas un objet), http_build_query(), et de la concaténation manuelle de strings.

La nouvelle extension Uri donne une vraie API orientée objet :

$uri = Uri\Uri::parse('https://example.com/path?query=value#fragment');
$modified = $uri->withPath('/new-path')->withQuery('key=val');
echo $modified; // https://example.com/new-path?key=val#fragment

Des objets-valeur immutables, un parsing conforme aux RFC, modifier des composants individuels sans parser et reconstruire la string entière. Attendu depuis longtemps.

#[\NoDiscard]

Un nouvel attribut qui génère un avertissement quand la valeur de retour est ignorée :

#[\NoDiscard("Use the returned collection, the original is unchanged")]
public function filter(callable $fn): static { ... }

Utile pour les méthodes immutables où ignorer la valeur de retour est presque certainement un bug. Courant dans d’autres langages depuis des années, maintenant en PHP où ça appartient.

clone with

Cloner un objet avec des propriétés modifiées sans utiliser de property hooks ni une méthode with() personnalisée :

$updated = clone($point) with { x: 10, y: 20 };

Syntaxe propre pour un pattern que les objets readonly nécessitaient : on clone pour “modifier” puisque la mutation directe n’est pas permise.

PHP 8.5 a une tendance fonctionnelle. L’opérateur pipe et l’extension URI ensemble rendent le code de transformation de données significativement plus lisible. Le langage continue dans une direction cohérente.

Les closures dans les expressions constantes

Une contrainte qui existait depuis PHP 5 : les expressions constantes (arguments d’attributs, valeurs par défaut de propriétés, valeurs par défaut de paramètres, déclarations const) ne pouvaient pas contenir de closures ni de callables de première classe. 8.5 supprime ça.

#[Validate(fn($v) => $v > 0)]
public int $count = 0;

const NORMALIZER = strtolower(...);

class Config {
    public function __construct(
        public readonly Closure $transform = trim(...),
    ) {}
}

C’est la pièce manquante qui rend les attributs vraiment expressifs pour les règles de validation et de transformation. Avant 8.5, il fallait passer des noms de classes ou des références string aux attributs et laisser le framework les retrouver. Maintenant le callable vit directement dans l’attribut.

Les attributs sur les constantes

L’attribut #[\Deprecated] de 8.4 ne pouvait pas être appliqué aux déclarations const. 8.5 ajoute le support des attributs pour les constantes en général :

const OLD_LIMIT = 100;

#[\Deprecated('Use RATE_LIMIT instead', since: '3.0')]
const API_TIMEOUT = 30;

const RATE_LIMIT = 60;

ReflectionConstant, une nouvelle classe de réflexion dans 8.5, expose getAttributes() pour que les outils puissent les lire. Combiné avec les closures dans les expressions constantes, les attributs sur les constantes deviennent une vraie couche de métadonnées pour les valeurs à la compilation.

#[\Override] s’étend aux propriétés

PHP 8.3 a apporté #[\Override] pour les méthodes. 8.5 l’étend aux propriétés :

class Base {
    public string $name = 'default';
}

class Derived extends Base {
    #[\Override]
    public string $name = 'derived';
}

Si la propriété n’existe pas dans le parent, PHP lève une erreur. Particulièrement utile avec les property hooks de 8.4 : on peut maintenant signaler qu’une propriété hookée surcharge intentionnellement celle d’un parent.

La visibilité asymétrique statique

8.4 a introduit la visibilité asymétrique (public private(set)) pour les propriétés d’instance. 8.5 l’apporte aussi aux propriétés static :

class Registry {
    public static private(set) array $items = [];

    public static function register(string $key, mixed $value): void {
        self::$items[$key] = $value;
    }
}

echo Registry::$items['foo']; // lisible
Registry::$items['bar'] = 1; // Error: cannot write outside class

Pattern direct : exposer une collection statique en lecture, bloquer la mutation externe.

La promotion de constructeur pour les propriétés final

La promotion de propriétés dans les constructeurs existe depuis PHP 8.0. Le modificateur final sur les propriétés promuées était la pièce manquante, 8.5 l’ajoute :

class ValueObject {
    public function __construct(
        public final readonly string $id,
        public final readonly string $name,
    ) {}
}

Une sous-classe ne peut pas surcharger $id ni $name avec une propriété du même nom. La combinaison final readonly sur les propriétés promuées rend les objets-valeur aussi verrouillés que possible sans sceller la classe entière.

Les casts dans les expressions constantes

Autre lacune dans les expressions constantes : pas de casts de type. 8.5 les permet :

const PRECISION = (int) 3.7;      // 3
const THRESHOLD = (float) '1.5';  // 1.5
const FLAG = (bool) 1;            // true

Ça semble mineur jusqu’à ce qu’on ait des constantes de configuration dérivées de variables d’environnement qui nécessitent une coercition de type directement à la déclaration.

Les erreurs fatales incluent les backtraces

Avant 8.5, une erreur fatale (mémoire insuffisante, dépassement de pile, erreur de type dans certains contextes) produisait un message sans contexte sur où dans le code ça s’est passé. Trouver la cause signifiait insérer des logs de debug et reproduire.

8.5 ajoute des backtraces à la stack aux messages d’erreur fatale, dans le même format que les backtraces d’exceptions. Une nouvelle directive INI, fatal_error_backtraces, contrôle le comportement. Elle est activée par défaut.

array_first() et array_last()

PHP avait reset() et end() pour accéder au premier et dernier élément d’un tableau depuis PHP 3. Les deux mutent le pointeur interne du tableau (pas sûr à appeler sur une référence), et ils retournent false pour les tableaux vides d’une façon indiscernable d’une valeur false stockée.

$values = [10, 20, 30];

$first = array_first($values);  // 10
$last  = array_last($values);   // 30

$first = array_first([]);       // null

Les nouvelles fonctions retournent null pour les tableaux vides, ne touchent pas le pointeur interne, et fonctionnent sur n’importe quelle expression de tableau sans avoir besoin d’une variable. reset($this->getItems()) était un avertissement de dépréciation en attente de se produire.

get_error_handler() et get_exception_handler()

PHP a set_error_handler() et set_exception_handler(). Obtenir le handler courant nécessitait soit de le stocker soi-même avant de le définir, soit d’appeler set_error_handler(null) et de capturer ce qui revenait, ce qui effaçait aussi le handler au passage.

8.5 ajoute :

$current = get_error_handler();
$current = get_exception_handler();

Pratique dans les chaînes de middleware où on veut envelopper le handler existant sans le perdre, ou dans les tests où on veut vérifier qu’un handler a bien été installé.

IntlListFormatter

Formater une liste avec les conjonctions appropriées à la locale nécessitait toujours un assemblage manuel de strings. 8.5 ajoute IntlListFormatter :

$formatter = new IntlListFormatter('en_US', IntlListFormatter::TYPE_AND);
echo $formatter->format(['apples', 'oranges', 'pears']);
// "apples, oranges, and pears"

$formatter = new IntlListFormatter('fr_FR', IntlListFormatter::TYPE_OR);
echo $formatter->format(['rouge', 'bleu', 'vert']);
// "rouge, bleu ou vert"

La classe enveloppe le ListFormatter d’ICU. Trois types : TYPE_AND, TYPE_OR, TYPE_UNITS. Les constantes de largeur contrôlent si on obtient “et” ou “&”. Gestion de la virgule d’Oxford, placement de conjonction spécifique à la locale, tout géré par ICU.

FILTER_THROW_ON_FAILURE pour filter_var()

filter_var() retourne false en cas d’échec de validation, ce qui produit l’ambiguïté classique false vs null vs 0 quand on filtre des entrées non fiables. Un nouveau flag change ça :

try {
    $email = filter_var($input, FILTER_VALIDATE_EMAIL, FILTER_THROW_ON_FAILURE);
} catch (Filter\FilterFailedException $e) {
    // explicitement invalide, pas faussement false
}

Les classes Filter\FilterFailedException et Filter\FilterException sont nouvelles dans 8.5. Le flag ne peut pas être combiné avec FILTER_NULL_ON_FAILURE : les comportements sont mutuellement exclusifs.

Les dépréciations qui nettoient des années de dette technique

L’opérateur backtick (`commande` comme alias de shell_exec()) est déprécié. C’est une syntaxe obscure qui surprend quiconque lit le code et est incohérente avec tous les autres appels de fonctions PHP.

Les noms de cast non canoniques ((boolean), (integer), (double), (binary)) sont dépréciés au profit de leurs formes courtes : (bool), (int), (float), (string). Les formes longues sont non documentées depuis des années ; 8.5 commence la suppression formelle.

Les instructions case terminées par un point-virgule sont dépréciées :

// déprécié
switch ($x) {
    case 1;
        break;
}

// correct
switch ($x) {
    case 1:
        break;
}

La forme avec point-virgule est syntaxiquement valide depuis PHP 4 mais personne ne l’utilise intentionnellement. C’est une faute de frappe que PHP acceptait.

__sleep() et __wakeup() sont dépréciés au profit de __serialize() et __unserialize(), qui retournent et reçoivent des tableaux et se composent correctement avec l’héritage. Les anciennes méthodes avaient une sémantique confuse autour de la visibilité des propriétés.

max_memory_limit plafonne les allocations incontrôlées

Une nouvelle directive INI accessible seulement au démarrage : max_memory_limit. Elle définit un plafond que memory_limit ne peut pas dépasser à l’exécution. Si un script appelle ini_set('memory_limit', '10G') et que max_memory_limit est à 512M, PHP avertit et plafonne la valeur.

Utile dans les environnements d’hébergement partagé, ou partout où on veut s’assurer qu’un bug ou un payload malveillant ne peut pas convaincre PHP d’élever sa propre limite et de dévorer toute la RAM de la machine.

Opcache est toujours présent

Dans 8.5, Opcache est toujours compilé dans le binaire PHP et toujours chargé. L’ancienne situation (Opcache comme extension chargeable qui pouvait ou non être présente selon la configuration de build) est révolue.

On peut toujours le désactiver : opcache.enable=0 fonctionne bien. Ce qui change, c’est la garantie que l’API Opcache (opcache_get_status(), opcache_invalidate(), etc.) est toujours disponible, quelle que soit la façon dont PHP a été compilé. Tout code qui vérifie extension_loaded('opcache') avant d’appeler les fonctions Opcache peut supprimer la vérification.