Fonctions anonymes et autres nouveautés PHP
Compléter la modernisation du code PHP avec les closures, les fonctions fléchées, l'opérateur nullsafe et les propriétés readonly
Notions théoriques
La séance précédente a présenté plusieurs syntaxes modernes de PHP 8 (match, ??, arguments nommés, promotion de propriété, enums). Cette séance complète ce panorama avec d'autres fonctionnalités très utilisées dans le code PHP actuel : les fonctions anonymes, les fonctions fléchées, l'opérateur de navigation sécurisée et les propriétés en lecture seule.
Les fonctions anonymes (closures)
Une fonction anonyme (ou closure) est une fonction sans nom, que l'on peut stocker dans une variable ou passer directement en argument à une autre fonction. Elle est très utile comme callback : une fonction que l'on transmet à une autre fonction pour qu'elle l'exécute au bon moment.
Par exemple, usort() trie un tableau en utilisant une fonction de comparaison fournie par le développeur :
$characters = [
['name' => 'Aldric', 'strength' => 45],
['name' => 'Lyra', 'strength' => 72],
['name' => 'Bram', 'strength' => 58],
];
usort($characters, function ($a, $b) {
return $b['strength'] <=> $a['strength'];
});
// $characters est maintenant trié par force décroissante
array_map() et array_filter() fonctionnent sur le même principe : elles reçoivent une fonction anonyme qui décrit ce qu'il faut faire de chaque élément du tableau.
Une fonction anonyme est une valeur comme une autre : on peut la stocker dans une variable, la passer en paramètre, ou la retourner depuis une autre fonction.
La clause use
Par défaut, une fonction anonyme ne connaît pas les variables définies en dehors d'elle. La clause use permet de capturer une variable externe pour l'utiliser à l'intérieur de la closure.
$minStrength = 50;
$strongCharacters = array_filter($characters, function ($character) use ($minStrength) {
return $character['strength'] >= $minStrength;
});
Par défaut, la variable est capturée par valeur : une copie est faite au moment de la déclaration de la closure. Il est possible de la capturer par référence avec use (&$variable) pour que les modifications faites dans la closure soient répercutées à l'extérieur.
Les fonctions fléchées (arrow functions, PHP 7.4)
Les fonctions fléchées (fn) sont une syntaxe plus courte pour écrire une fonction anonyme d'une seule expression. Elles ont deux particularités par rapport aux closures classiques :
- Le corps est une expression unique, dont le résultat est automatiquement retourné (pas de mot-clé
return, pas d'accolades) - Les variables externes sont capturées automatiquement, sans avoir besoin de
use
// Avec une closure classique
$strongCharacters = array_filter($characters, function ($character) use ($minStrength) {
return $character['strength'] >= $minStrength;
});
// Avec une fonction fléchée
$strongCharacters = array_filter($characters, fn($character) => $character['strength'] >= $minStrength);
Utilisez une fonction fléchée quand le traitement tient en une seule expression. Revenez à une closure classique dès que la logique nécessite plusieurs instructions.
L'opérateur de navigation sécurisée (nullsafe ?->, PHP 8)
Accéder à une propriété ou appeler une méthode sur une chaîne d'objets potentiellement null oblige normalement à vérifier chaque maillon avec isset() ou ??, ce qui alourdit le code :
// Avant : vérification manuelle
$weaponName = ($character->getEquippedWeapon() !== null)
? $character->getEquippedWeapon()->getName()
: 'Aucune arme';
L'opérateur nullsafe ?-> remplace -> et arrête immédiatement la chaîne d'appels en retournant null dès qu'un maillon est null, sans déclencher d'erreur :
// Après : opérateur nullsafe
$weaponName = $character->getEquippedWeapon()?->getName() ?? 'Aucune arme';
Si getEquippedWeapon() retourne null, l'expression entière s'arrête et retourne null ; l'opérateur ?? prend ensuite le relais pour fournir une valeur par défaut.
Vous avez déjà croisé ?-> dans la séance précédente, dans l'exemple $player?->getName() ?? 'Inconnu', sans qu'il soit expliqué en détail. C'est précisément ce que fait cette section.
Les propriétés en lecture seule (readonly, PHP 8.1)
Une propriété déclarée readonly ne peut être assignée qu'une seule fois, typiquement dans le constructeur. Toute tentative de modification ultérieure déclenche une erreur. C'est très utile pour créer des objets immuables, dont l'état ne doit jamais changer après leur création — par exemple l'identifiant d'un personnage.
class Character
{
public function __construct(
public readonly int $id,
public string $name,
) {}
}
$character = new Character(1, 'Aldric');
$character->name = 'Bram'; // autorisé : $name n'est pas readonly
$character->id = 2; // erreur : impossible de modifier une propriété readonly
readonly se combine très bien avec la promotion de propriété dans le constructeur, mais rien n'oblige à les utiliser ensemble : une propriété readonly peut aussi être déclarée classiquement.
Exemple de mise en application
Reprenons notre jeu de personnages. On veut afficher, pour une équipe, le nom des personnages suffisamment forts, triés du plus fort au plus faible, ainsi que l'arme équipée si elle existe :
$characters = [
new Character(1, 'Aldric', strength: 45, weapon: null),
new Character(2, 'Lyra', strength: 72, weapon: new Weapon('Épée longue')),
new Character(3, 'Bram', strength: 58, weapon: new Weapon('Hache')),
];
// Tri par force décroissante avec une closure
usort($characters, function ($a, $b) {
return $b->getStrength() <=> $a->getStrength();
});
// Filtrage des personnages assez forts avec une fonction fléchée
$strongCharacters = array_filter($characters, fn($character) => $character->getStrength() >= 50);
foreach ($strongCharacters as $character) {
$weaponName = $character->getWeapon()?->getName() ?? 'Aucune arme';
echo $character->getName() . ' (' . $weaponName . ")\n";
}
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Complétez le code de gestion d'une équipe de personnages avec les nouvelles syntaxes de cette séance
Étape 1 — Trier l'équipe par force décroissante avec une closure
<=>) dans les callbacks de triL'opérateur <=> retourne -1, 0 ou 1 selon que la valeur de gauche est inférieure, égale ou supérieure à celle de droite.
Il rend les fonctions de comparaison passées à usort() beaucoup plus lisibles qu'une succession de if.
Étape 2 — Filtrer les personnages assez forts avec une fonction fléchée
Dès qu'un callback tient en une seule expression, une fonction fléchée rend le code plus court et plus lisible
qu'une closure avec use. Réservez les closures classiques aux traitements qui nécessitent plusieurs instructions.
Étape 3 — Sécuriser l'accès à l'arme équipée avec l'opérateur nullsafe
?-> et ??L'opérateur nullsafe ?-> gère l'absence d'objet intermédiaire, mais ?? reste nécessaire pour fournir
une valeur par défaut si le résultat final est null. Les deux opérateurs se complètent, ils ne se remplacent pas.
Étape 4 — Rendre l'identifiant du personnage immuable
Un identifiant ($id), une date de création, ou toute donnée qui ne doit jamais changer après la construction
de l'objet est un excellent candidat pour readonly. Cela évite des bugs où une propriété serait modifiée
par erreur ailleurs dans le code.