Aller au contenu principal
La newsletter arrive ! Premier envoi très bientôt : laissez votre adresse pour la recevoir.

PHP 8.4 : rattrapage de retard ou véritable modernisation ?.

PHP 8.4 introduit les property hooks, la visibilité asymétrique, et des fonctions de tableau. Analyse de leur adoption et impact réel sur les projets.

MAJ 6 min de lecture
Sommaire · 6

PHP 8.4 rattrape des langages qui avaient quinze ans d'avance. Sur ce blog, ses nouveautés se sont installées de façon très inégale : 32 property hooks tous regroupés au même endroit, 6 fonctions de tableau, 2 visibilités asymétriques, et zéro pour les deux fonctionnalités les plus annoncées.

Ce que 8.4 a laissé, un an et demi après

PHP 8.4 est sortie en novembre 2024, sous le signe du rattrapage. Property hooks comme en C# ou en Kotlin, visibilité asymétrique comme en Swift, parser HTML5 conforme comme dans n'importe quel navigateur.

Le relevé sur le src/ de ce blog, qui tourne en PHP 8.5 :

  • property hooks : 32
  • array_find, array_any, array_all : 6
  • instanciation sans parenthèses new X()-> : 4
  • visibilité asymétrique private(set) : 2
  • lazy objects : 0
  • #[\Deprecated] : 0

Rien de spectaculaire dans ces nombres. Ce qui est intéressant, c'est leur répartition : les 32 property hooks ne sont pas éparpillés dans le projet, ils sont presque tous au même endroit.

Les property hooks ont atterri à un seul endroit

Un property hook attache une lecture ou une écriture à une propriété, au lieu qu'on passe par une méthode nommée. $objet->prop déclenche du code, sans que l'appelant sache qu'il y a du code.

Les 32 usages de ce dépôt vivent presque tous dans les composants Twig qui rendent les blocs de contenu. La raison est précise :

PHP
// src/Twig/Component/Block/Image.php
public ?Media $media {
    get => $this->mediaResolver->resolve($this->mediaId);
}

public string $mediaUrl {
    get => $this->media instanceof Media ? $this->mediaUrlGenerator->generate($this->media) : '';
}

Un gabarit Twig écrit {{ this.mediaUrl }}. Il ne peut pas passer d'arguments, et il ne devrait pas avoir à savoir si la valeur est stockée ou calculée. Avant 8.4, obtenir une propriété calculée dans un composant demandait une méthode publique et une convention de nommage que le gabarit devait connaître. Le hook supprime la convention : la propriété est calculée, et le gabarit ne voit qu'une propriété.

C'est la seule surface du projet où le problème se posait vraiment, et c'est là que la fonctionnalité s'est concentrée. Une nouveauté ne se répand pas partout un peu, elle se concentre là où elle supprime un geste.

Les hooks appliqués aux entités Doctrine posent une autre question, plus épineuse, et elle a son billet : PHP 8.4 Property Hooks : quand Doctrine 3.4 dépoussière vos getters/setters.

La visibilité asymétrique : deux usages, et le cas d'école qui va avec

public private(set) expose une propriété en lecture à tout le monde, et réserve l'écriture à la classe. Deux usages ici, sur le même trait, et ils illustrent exactement le besoin :

PHP
// src/Entity/Concerns/TimestampableTrait.php
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
public private(set) ?\DateTimeImmutable $createdAt = null;

#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
public private(set) ?\DateTimeImmutable $updatedAt = null;

Une date de création se lit partout et ne s'écrit qu'une fois, par le code qui gère le cycle de vie. Avant, on écrivait une propriété privée plus un accesseur, soit quatre lignes pour dire « lisible, pas modifiable ». La forme asymétrique le dit en un mot, à l'endroit de la déclaration.

Deux usages seulement, parce que le reste du projet règle la question autrement : les services sont final readonly, donc rien n'est modifiable du tout. La visibilité asymétrique sert là où l'immutabilité totale est impossible, et sur ce dépôt, ce sont les entités Doctrine.

array_find, array_any, array_all : le retour des fonctions qui manquaient

Ces trois fonctions auraient dû exister depuis quinze ans. Pour savoir si un tableau contient au moins un élément satisfaisant une condition, on écrivait count(array_filter($arr, $fn)) > 0, ce qui parcourt tout le tableau et alloue un tableau intermédiaire pour répondre à une question booléenne.

Six usages ici, et ils remplacent tous ce motif :

PHP
// src/ValueObject/Content/BlocksDocument.php
return array_any($this->blocks, static fn(array $block): bool => $block['type'] === $type);

array_any s'arrête au premier élément qui correspond. Sur un document qui peut porter cent blocs, la différence n'est pas théorique. Mais on gagne surtout ailleurs : la ligne dit ce qu'elle fait. count(array_filter(...)) > 0 oblige le lecteur à reconstruire l'intention à partir d'un moyen.

Ce qui n'a pas pris, et pourquoi

Les lazy objects permettent de différer la construction d'un objet entier jusqu'au premier accès. Zéro usage direct ici, ce qui n'empêche pas d'en dépendre : c'est sur eux que Symfony s'appuie désormais pour ses services différés, et c'est grâce à eux qu'un service final readonly peut enfin être déclaré lazy, ce qui était impossible avant. La fonctionnalité travaille sous le framework, pas dans le code applicatif.

#[\Deprecated] marque une fonction ou une constante comme obsolète, et le moteur émet l'avertissement. Zéro usage, pour une raison qui n'a rien de technique : cet attribut sert à qui publie du code que d'autres consomment. Une application n'a personne à prévenir. Elle supprime.

Le parser HTML5 de l'extension DOM comble une lacune ancienne : DOMDocument::loadHTML() interprétait le HTML selon des règles antérieures à HTML5, d'où ces avertissements qu'on a tous masqués avec un @ à un moment ou à un autre. Ce projet n'en a pas l'usage, sa sanitization passant par le composant Symfony dédié, mais c'est probablement la correction la plus utile de la version pour qui manipule du HTML venu de l'extérieur.

Rattrapage, donc, et ce n'est pas un reproche

La question du titre appelle une réponse simple, et je la donne sans détour : c'est bien du rattrapage. Property hooks, visibilité asymétrique, fonctions de tableau, parser conforme, toutes ces choses existaient ailleurs depuis longtemps.

Ce que le relevé ajoute, c'est qu'on ne mesure pas un rattrapage comme on mesure une innovation. Une fonctionnalité nouvelle cherche son usage pendant des années. Une fonctionnalité de rattrapage arrive sur un besoin déjà identifié, déjà contourné, souvent avec un contournement laid que tout le monde connaît. Elle ne se diffuse pas lentement dans tout le code : elle va droit là où le contournement vivait, et elle l'efface d'un coup.

C'est pour ça que les 32 hooks sont groupés au même endroit plutôt que répartis, et pourquoi les 6 array_any remplacent tous exactement la même formule. Une version de rattrapage ne change pas la façon d'écrire. Elle supprime des cicatrices.

Reste à savoir laquelle vous portez encore sans y penser, et depuis assez longtemps pour ne plus la voir.

Une coquille, une erreur dans ce billet ? Signale-la-moi.

Ce billet est publié sous licence Creative Commons BY-NC-SA 4.0 (attribution, pas d'usage commercial, partage dans les mêmes conditions).

Vous aimerez aussi

Newsletter

Un mail quand il y a quelque chose à dire

Pas de tracking, pas de pub, pas de "10 astuces pour…". Juste les nouveaux billets et parfois une réflexion en plus.

Activez uniquement ce que vous souhaitez. Vos choix sont conservés 6 mois.

Strictement nécessaires

Indispensables au fonctionnement du site (session, sécurité, préférence d'affichage). Aucune donnée n'est partagée à des tiers et aucun consentement n'est requis.

Toujours actif

Mesure d'audience

Statistiques via Google Analytics (GA4) : pages vues, source du trafic, navigateur et interactions clés. Dépose des cookies de mesure, activés seulement avec votre accord (Consent Mode). Sans publicité ciblée, sans Google Signals, sans partage commercial.

Contenus externes

Affiche les GIF animés hébergés par Giphy (CDN aux États-Unis). À l'affichage d'un GIF, votre adresse IP et votre navigateur sont transmis à Giphy. Sans votre accord, les GIF ne s'affichent pas.