Fonctions et closures
Les fonctions PHP ressemblent Ă celles de TypeScript avec un $ devant chaque paramĂštre, et les closures capturent par valeur, pas par variable. La premiĂšre moitiĂ© de cette phrase se lit en vitesse, et câest dans la seconde que se cachent les surprises de ce chapitre.
<?php
declare(strict_types=1);
function greet(string $name, string $greeting = 'Hello', bool $shout = false): string
{
$text = "$greeting, $name!";
return $shout ? strtoupper($text) : $text;
}
echo greet('Ada'); // Hello, Ada!
echo greet('Ada', shout: true); // HELLO, ADA!
echo greet(greeting: 'Hi', name: 'Ada'); // Hi, Ada!
ParamĂštres et valeurs de retour portent des types, vĂ©rifiĂ©s Ă lâexĂ©cution comme lâexplique Le systĂšme de types. Les valeurs par dĂ©faut fonctionnent comme partout. Les arguments nommĂ©s (PHP 8.0) permettent de sauter les valeurs par dĂ©faut qui ne vous intĂ©ressent pas et rendent lisible un appel Ă quatre boolĂ©ens. Positionnels et nommĂ©s se mĂ©langent, les positionnels dâabord.
Variadiques, références et types de retour particuliers
... sur le dernier paramÚtre rassemble le reste dans un tableau ; ... dans un appel déplie un tableau en arguments, clés de chaßne comprises, ce qui transforme un tableau en arguments nommés :
<?php
declare(strict_types=1);
function sum(int ...$numbers): int
{
return array_sum($numbers);
}
echo sum(1, 2, 3); // 6
echo sum(...[4, 5]); // 9
$options = ['greeting' => 'Hey', 'name' => 'Ada'];
// greet(...$options) would call greet(name: 'Ada', greeting: 'Hey')
Un paramĂštre dĂ©clarĂ© &$x reçoit une rĂ©fĂ©rence, et la fonction Ă©crit alors directement dans la variable de lâappelant. La bibliothĂšque standard sâen sert pour sort(), pour le $matches de preg_match() et pour quelques autres. Dans votre propre code, prĂ©fĂ©rez renvoyer la valeur, parce quâune fonction qui modifie ses arguments oblige le lecteur Ă lâouvrir pour comprendre ce quâelle fait.
Les types de paramĂštres acceptent tout ce que le systĂšme de types propose : ?string $label = null pour une valeur optionnelle, int|string $id pour une union, Countable&Traversable $items pour une intersection. Ăcrivez le ? explicitement, car un simple string $x = null fonctionne encore mais est dĂ©prĂ©ciĂ© depuis PHP 8.4. Pour une fonction qui accepte une fonction, vous avez le choix entre callable, qui accepte les closures ainsi que les formes chaĂźne et tableau dĂ©crites plus bas, et Closure, qui nâaccepte que de vrais objets closure. Le code rĂ©cent tend Ă dĂ©clarer Closure et Ă laisser lâappelant convertir avec (...), parce quâune Closure se vĂ©rifie par le typage alors quâune chaĂźne Ă©chappe Ă toute vĂ©rification.
Les fonctions qui ne reviennent pas normalement disposent de leurs propres types de retour : void signifie que rien ne revient, et never (PHP 8.1) signifie que la fonction lÚve toujours une exception ou termine le script, de sorte que les analyseurs statiques savent que le code placé aprÚs un appel à fail() est inaccessible.
Les fonctions ne sont pas des valeurs, mais on peut en obtenir une référence
Un nom de fonction nâest pas une expression, et $f = strlen; est une erreur de syntaxe. Le contournement historique passait par une chaĂźne : $f = 'strlen'; fonctionne parce que le type callable accepte un nom de fonction, une paire [$object, 'method'] ou une chaĂźne 'Class::method'. Cette forme marche encore, mais rien ne la vĂ©rifie avant lâexĂ©cution.
La façon moderne est la syntaxe de callable de premiÚre classe (PHP 8.1) : le nom suivi de (...).
<?php
declare(strict_types=1);
final class Mailer
{
public function send(string $to): string
{
return "sent to $to";
}
}
$length = strlen(...); // Closure wrapping strlen()
$send = (new Mailer())->send(...); // Closure bound to that instance
echo $length('hello'); // 5
echo $send('ada@example.org');
var_dump(array_map(strtoupper(...), ['a', 'b'])); // ['A', 'B']
Le rĂ©sultat est un objet Closure, la seule valeur de type fonction en PHP. Il est sĂ»r pour le typage comme pour le refactoring, puisque votre Ă©diteur suit le renommage de la mĂ©thode, et câest la forme Ă passer Ă array_map() et consorts. Quand vous tenez une chaĂźne plutĂŽt quâun nom, Closure::fromCallable('strlen') produit le mĂȘme objet.
Le rĂ©flexe du dĂ©veloppeur venu dâailleurs est dâĂ©crire
$this->sendsans parenthĂšses, ce qui en PHP lit la propriĂ©tĂ©send, qui nâexiste pas. La mĂ©thode en tant que valeur sâĂ©crit$this->send(...).
Les closures capturent par valeur
Les fonctions anonymes existent, et vous devez dire ce quâelles capturent :
<?php
declare(strict_types=1);
$rate = 0.2;
$withTax = function (float $price) use ($rate): float {
return $price * (1 + $rate);
};
$rate = 0.5; // too late, the closure already copied 0.2
echo $withTax(100.0); // 120
La clause use copie les variables au moment oĂč la closure est créée. Rien de la portĂ©e englobante nâest visible sans ĂȘtre listĂ©, et les changements ultĂ©rieurs de la variable extĂ©rieure nâatteignent pas la closure. LĂ oĂč JavaScript et Python se referment sur la variable elle-mĂȘme et afficheraient 150 ici, PHP donne Ă la closure un instantanĂ© pris Ă sa crĂ©ation.
Pour capturer la variable plutĂŽt que sa valeur, ajoutez & dans la clause, use (&$rate), et la closure partage alors la variable avec la portĂ©e extĂ©rieure, dans les deux sens. On en a besoin pour un accumulateur, ou pour une closure rĂ©cursive qui doit se voir elle-mĂȘme :
$fact = function (int $n) use (&$fact): int {
return $n <= 1 ? 1 : $n * $fact($n - 1);
};
Les fonctions flĂ©chĂ©es (PHP 7.4) suppriment la cĂ©rĂ©monie. fn capture automatiquement toute la portĂ©e englobante, par valeur, et ne contient quâune seule expression :
$withTax = fn(float $price): float => $price * (1 + $rate);
Il nây a plus ni use, ni return, ni accolades, mais la sĂ©mantique dâinstantanĂ© reste la mĂȘme. La plupart des callbacks que vous Ă©crirez seront des fonctions flĂ©chĂ©es, et vous reviendrez Ă function () use () quand il vous faudra plusieurs instructions ou une capture par rĂ©fĂ©rence.
Dans une classe, une closure conserve $this automatiquement, comme vous vous y attendez. Marquez-la static fn ou static function quand elle nâa pas besoin de lâinstance, ce qui Ă©vite de maintenir lâobjet en vie depuis un callback de longue durĂ©e. Closure::bind() et $closure->call($object) rattachent $this Ă un autre objet ; câest ainsi que les frameworks atteignent un Ă©tat privĂ© depuis lâextĂ©rieur, et vous les Ă©crirez rarement vous-mĂȘme.
Générateurs
Une fonction qui contient yield renvoie un Generator sans exĂ©cuter son corps, et chaque tour de foreach la fait avancer jusquâau yield suivant. Si vous connaissez les gĂ©nĂ©rateurs de Python, vous les retrouvez ici presque ligne pour ligne :
<?php
declare(strict_types=1);
/** @return Generator<int, string> */
function lines(string $path): Generator
{
$handle = fopen($path, 'r');
try {
while (($line = fgets($handle)) !== false) {
yield rtrim($line, "\n");
}
} finally {
fclose($handle);
}
}
foreach (lines('/etc/hosts') as $number => $line) {
echo "$number: $line", PHP_EOL;
}
Le fichier est lu une ligne Ă la fois, quelle que soit sa taille, et fermĂ© quand la boucle se termine ou sâinterrompt. Un gĂ©nĂ©rateur est iterable, donc toute fonction qui accepte iterable le prend sans le savoir. yield $key => $value fixe des clĂ©s explicites, yield from dĂ©lĂšgue Ă un autre gĂ©nĂ©rateur ou Ă un tableau, et un return dans un gĂ©nĂ©rateur dĂ©finit une valeur lisible via getReturn() une fois lâitĂ©ration terminĂ©e. Un gĂ©nĂ©rateur ne sâexĂ©cute quâune fois, et pour itĂ©rer Ă nouveau il faut rappeler la fonction.
Pipelines
PHP 8.5 ajoute lâopĂ©rateur pipe. $x |> f(...) appelle f($x), et les chaĂźnes se lisent de haut en bas au lieu de lâintĂ©rieur vers lâextĂ©rieur :
// PHP 8.5
$slug = ' Hello World '
|> trim(...)
|> strtolower(...)
|> (fn(string $s) => str_replace(' ', '-', $s));
echo $slug; // hello-world
Chaque Ă©tape est nâimporte quel callable Ă un argument, ce qui correspond exactement Ă ce que produisent la syntaxe de callable de premiĂšre classe et les fonctions flĂ©chĂ©es. Avant 8.5, le mĂȘme code tenait en trois appels imbriquĂ©s ou en trois variables temporaires, deux formes qui fonctionnent toujours et restent courantes.
PHP 8.5 apporte aussi #[\NoDiscard], un attribut pour les fonctions dont la valeur de retour ne doit pas ĂȘtre ignorĂ©e. Si vous appelez une telle fonction comme simple instruction, PHP Ă©met un avertissement, et vous transtypez lâappel en (void) pour dire que câest voulu. Les bibliothĂšques lâutilisent sur les mĂ©thodes qui renvoient un nouvel objet immuable, pour Ă©viter le bug classique du $date->modify() dont on jette le rĂ©sultat.
Les formes anciennes que vous reconnaĂźtrez
func_get_args() et func_num_args() lisent les arguments dâune fonction dĂ©clarĂ©e sans paramĂštres ; elles prĂ©cĂšdent ...$args et survivent dans le vieux code. call_user_func() et call_user_func_array() invoquent un callable, ce que $callable(...$args) fait aujourdâhui. create_function() fabriquait des closures Ă partir de chaĂźnes et a Ă©tĂ© supprimĂ©e en 8.0. Quand vous croisez ces formes, le remplacement moderne se trouve Ă une ligne de lĂ , et Rector peut faire la modification pour vous.
Les closures photographient leurs variables
use, les fonctions flĂ©chĂ©es photographient toute la portĂ©e, et une mĂ©thode devient une valeur avec(...). Avec ces trois points en tĂȘte, les callbacks PHP nâont plus de surprise en rĂ©serve.
Les fonctions portent le comportement, mais les donnĂ©es sur lesquelles elles agissent sont surtout des objets, et le modĂšle objet de PHP a plus changĂ© ces cinq derniĂšres annĂ©es que pendant les quinze prĂ©cĂ©dentes. Le chapitre Les classes montre Ă quoi il ressemble aujourdâhui.