Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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->send sans 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.

Une closure en cours de crĂ©ation prend une photo de la variable rate qui affiche 0.2 ; ensuite, le rate extĂ©rieur passe Ă  0.5 sur le bureau, mais la closure tient toujours la photo oĂč on lit 0.2

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.