PHP 8.4 : property hooks et visibilité asymétrique en pratique

PHP 8.4 apporte deux nouveautés qui changent la façon d'écrire des classes : les property hooks, qui attachent du code à la lecture ou à l'écriture d'une propriété, et la visibilité asymétrique, qui permet de lire une propriété publiquement tout en limitant qui peut la modifier.

Les property hooks

Un hook set valide ou transforme la valeur avant de la stocker ; un hook get peut calculer une propriété « virtuelle » sans stockage :

<?php

final class Utilisateur
{
    public string $email {
        set (string $valeur) {
            if (!filter_var($valeur, FILTER_VALIDATE_EMAIL)) {
                throw new InvalidArgumentException("Adresse e-mail invalide : $valeur");
            }
            $this->email = strtolower($valeur);
        }
    }

    // Propriété virtuelle : calculée à chaque lecture, jamais stockée.
    public string $nomComplet {
        get => "{$this->prenom} {$this->nom}";
    }

    public function __construct(
        public string $prenom,
        public string $nom,
        string $email,
    ) {
        $this->email = $email;
    }
}

$u = new Utilisateur('Ada', 'Lovelace', 'Ada@Example.org');
echo $u->email;      // ada@example.org
echo $u->nomComplet; // Ada Lovelace

Le code appelant ne change pas : on continue d'écrire $u->email = '…'. On peut donc ajouter une validation à une propriété publique existante sans casser son API.

La visibilité asymétrique

Cas classique : un solde lisible par tout le monde mais modifiable uniquement par la classe elle-même. Avant, il fallait une propriété privée et un getter. Le diff parle de lui-même :

 final class Compte
 {
-    private int $solde = 0;
-
-    public function getSolde(): int
-    {
-        return $this->solde;
-    }
+    public private(set) int $solde = 0;

     public function crediter(int $montant): void
     {
         $this->solde += $montant;
     }
 }

Une écriture depuis l'extérieur lève désormais une Error : « Cannot modify private(set) property Compte::$solde from global scope ».

Bonus : de nouvelles fonctions pour les tableaux

array_find(), array_find_key(), array_any() et array_all() évitent enfin les boucles foreach écrites pour trouver un élément :

$commandes = [
    ['id' => 1, 'montant' => 45, 'payee' => true],
    ['id' => 2, 'montant' => 180, 'payee' => false],
    ['id' => 3, 'montant' => 320, 'payee' => true],
];

$grosse = array_find($commandes, fn (array $c) => $c['montant'] > 100);   // commande n° 2
$impayee = array_any($commandes, fn (array $c) => !$c['payee']);          // true
$toutesPayees = array_all($commandes, fn (array $c) => $c['payee']);      // false

// Et plus besoin de parenthèses autour de « new » pour chaîner un appel :
$date = new DateTimeImmutable()->format('d/m/Y');

Pour migrer un projet existant, Rector propose un jeu de règles PHP 8.4 qui fait une bonne partie du travail.