Une lectrice m'a envoyé un diff la semaine dernière, avec une ligne entourée en rouge : `$tag = sprintf('[%s] %s', currentRequestId(), ?);`. Sa question : « Est-ce que c'est pareil que la fonction fléchée que ça remplace ? » Non, et cette seule ligne te dit presque tout ce qu'il faut savoir sur la Partial Function Application de PHP 8.6. J'aime beaucoup cette fonctionnalité et je la veux dans nos bases de code dès le premier jour. Je pense aussi que la plupart d'entre nous vont mal la lire pendant six mois, parce qu'elle ressemble à du sucre syntaxique pour `fn` alors qu'elle se comporte comme un constructeur qui prend un instantané.

Petit rappel si tu as zappé les fils de discussion sur la RFC. PHP 8.6 est en bêta, avec une sortie prévue autour du 19 novembre 2026. La PFA s'appuie sur la syntaxe de callable de première classe `trim(...)` arrivée en 8.1. Tu appelles une fonction ou une méthode normalement, mais tu mets un `?` dans chaque emplacement que tu veux remplir plus tard, et tu récupères un callable. Un `?` seul représente exactement un argument. Un `...` final signifie tous les arguments restants, et il doit venir en dernier, après les emplacements positionnels et les nommés. Si tu écris `str_replace(' ', '-', ?)`, tu obtiens un slugifier sans closure, sans nom de paramètre à inventer et sans `$value` répété deux fois. Ça marche aussi sur les méthodes statiques et d'instance, et ça s'entend bien avec les arguments nommés, si bien que `Book::create(category: Category::THRILLER, title: ?)` se lit presque comme de la configuration.

C'est la moitié manquante de l'opérateur pipe. Depuis l'arrivée de `|>`, chaque pipeline que j'écrivais avait un maillon moche : la fonction fléchée entre parenthèses dont tu as besoin dès qu'une étape prend plus d'un argument. Cinq étapes bien propres, puis un `(fn ($v) => str_replace(' ', '-', $v))` planté au milieu comme un collier de serrage. Avec la PFA, cette étape devient `str_replace(' ', '-', ?)` et le `?` montre exactement où la valeur entre. Les gens de Go aiment nous répéter que les boucles explicites valent mieux que les enchaînements astucieux, et ils n'ont pas tort. Mais dès qu'un langage a un pipe, le pipe a besoin de ça, et 8.6 le livre.

Revenons à ce cercle rouge. Un partial évalue chaque argument qui n'est pas un placeholder au moment où tu le crées. La valeur est capturée puis réutilisée à chaque appel. Donc `currentRequestId()`, dans la ligne de ma lectrice, ne s'est exécutée qu'une seule fois, quand le worker a démarré et construit son formateur. La fonction fléchée qu'elle remplaçait appelait cette fonction à chaque ligne de log. Dans une requête FPM classique, personne ne remarque rien, parce que le partial et la requête meurent ensemble. Dans un worker Swoole, RoadRunner ou de file d'attente qui tourne longtemps, chaque ligne de log des quarante mille jobs suivants porte l'ID du premier job. Ton ingénieur d'astreinte le découvre à 2 h du matin, en faisant un grep sur une requête qui semble avoir traité tout le trafic de la nuit.

L'objection la plus solide mérite d'être prise au sérieux. Tu pourrais dire que ça prouve que la PFA est un piège, que `?` est un sigil de plus dans un langage qui en a déjà plein, et qu'une closure explicite est plus honnête parce qu'elle montre exactement ce qui s'exécute et quand. Je suis à moitié d'accord. Une closure rend le moment d'exécution évident, un partial le cache. Mais la capture immédiate est la bonne sémantique, et c'est souvent ce que tu veux vraiment : construire le truc coûteux une fois, l'appeler plein de fois. La version closure qui relance `getPrefix()` à chaque appel a ses propres bugs, ils ont juste une autre tête. Ce que je retiens de l'objection, c'est qu'il nous faut une habitude de relecture. Personne n'a besoin d'interdire la syntaxe.

Le deuxième piège est plus discret. Un partial est une closure toute neuve. Il ne se contente pas de pointer vers la fonction d'origine comme le fait `strlen(...)`. Les attributs personnalisés de l'original ne sont pas reportés. Les exceptions sont `#[\NoDiscard]` et `#[\SensitiveParameter]`, et ce dernier ne survit que sur les paramètres que tu as laissés ouverts. Si ton framework inspecte les callables par réflexion pour trouver des métadonnées de route, des règles de validation ou des marqueurs d'event listener, un partial passera au travers sans bruit. Les équipes qui câblent leur conteneur à coups d'attributs devraient faire un grep avant de laisser qui que ce soit confier des partials au registre.

Voici donc la règle maison que je propose à ma propre équipe. Un `?` ou un `...` final, c'est OK partout où les arguments fixes sont des littéraux, des constantes, des cas d'enum ou des valeurs déjà rangées dans des variables locales. Dès qu'un argument fixe est un appel de fonction, un appel de méthode ou quoi que ce soit qui lit l'horloge, la requête ou le conteneur, l'auteur le remonte dans une variable nommée sur la ligne du dessus, pour que la capture soit visible, ou écrit une closure exprès. Les partials ne vont jamais là où Reflection lit des attributs. Ça nous coûte une ligne de plus de temps en temps, et en échange personne n'a à deviner quand un argument est évalué.

Cela dit, je ne suis pas sûr que cette règle soit la bonne. Elle est peut-être trop prudente pour les pipelines, où presque tous les arguments fixes sont de toute façon des littéraux. Elle est peut-être trop laxiste pour les workers qui tournent longtemps. Alors dis-moi en commentaire : quand 8.6 sortira en novembre, ton équipe autorisera-t-elle les appels de fonction comme arguments fixes dans un partial, ou exigera-t-elle que les valeurs capturées soient d'abord remontées dans une variable ?