Aller au contenu principal

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.

info

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);
astuce

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.

info

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
attention

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


Qu'est-ce qu'une fonction anonyme (closure) en PHP ?


À quoi sert la clause `use` dans une fonction anonyme classique ?


Quelle est la principale différence entre une closure classique et une fonction fléchée (`fn`) ?


Que retourne le corps d'une fonction fléchée ?


Que fait l'opérateur nullsafe `?->` quand l'objet à gauche vaut `null` ?


Depuis quelle version de PHP l'opérateur nullsafe `?->` est-il disponible ?


Que se passe-t-il si on tente de réassigner une propriété `readonly` en dehors du constructeur ?


Depuis quelle version de PHP les propriétés `readonly` sont-elles disponibles ?



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


Bonne pratique - Utiliser l'opérateur spaceship (<=>) dans les callbacks de tri

L'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


Bonne pratique - Préférer les fonctions fléchées pour les traitements courts

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


Bonne pratique - Combiner ?-> 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


Bonne pratique - Rendre readonly les propriétés qui identifient un objet

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.

📌 Une solution