⚡ Outils d'optimisation PHP : application partielle de fonctions, jetons et flux
le 13 septembre 2026
Au moment où j’écris ces lignes, PHP 8.6 n’est pas encore une version stable destinée à la production. Les expériences ont été menées sur la version 8.6.0beta2, isolée de la CLI PHP 8.5.4 hôte, qui ne peut même pas analyser foo(?). Considérez chaque affirmation comme un comportement pré-version que j’ai effectivement testé, et non comme une promesse concernant le mois de novembre.
La première question que je me suis posée était toujours celle-ci :
Qu’adviendra-t-il de Flow lorsque PHP lui-même deviendra plus performant pour exprimer des pipelines fonctionnels ?
C’est désormais la première expérience au sein d’une expérience plus vaste :
Quelle quantité d’outils le PHP moderne peut-il fournir avant que nous ayons besoin d’une autre abstraction ?
Trois séries de tests indépendantes sont parvenues à la même conclusion.
Besoin d’une liaison d’arguments ? → PFA
Besoin d’une structure source légère ? → les tokens PHP
Besoin d’itérer sur des fichiers locaux ? → foreach
Besoin d’une véritable orchestration ? → envisagez alors Flow
Utilisez d’abord la primitive PHP. N’ajoutez de l’abstraction et de l’orchestration que lorsqu’elles ont fait leurs preuves.
1. De combien d’outils avons-nous réellement besoin ?
Darkwood Flow était un bon endroit pour dissimuler un article sur les fonctionnalités du langage, mais un bien moins bon endroit pour s’arrêter là. Une fois que l’application partielle de fonction a produit une Closure que Flow acceptait déjà, la question intéressante qui restait n’était pas « qu’y a-t-il d’autre dans la version 8.6 ? » mais de savoir si cette même habitude — se tourner vers une fonction d’aide, un paquet, un framework — se manifestait un niveau plus bas, dans les outils que nous écrivons à propos de PHP.
Cet article ne prétend pas que PHP soit plus rapide que Rust ou Go. Il ne prétend pas non plus que les tokens l’emportent sur les AST. Il s’agit de trois mesures d’une question plus simple : l’abstraction suivante a-t-elle mérité sa place ?
2. PHP 8.6 supprime l’adaptateur de fermeture
Le coût n’est pas « le fait d’avoir écrit une fonction ». Le surcoût, c’est la fonction supplémentaire que vous écrivez uniquement pour qu’une autre fonction puisse accéder à un argument restant.
fn ($value) => transform($value, $configuration)
L’application partielle de fonction (Partial Function Application) de PHP 8.6 est la primitive native :
transform(?, $configuration)
Le fichier officiel de Flow examples/flow.php ne présente pas ce surcoût. Ces tâches sont de véritables corps de fonction. Les adaptateurs se trouvent dans le code consommateur. La classe LanguagePipelineFactory de nolife-language s'applique à un corpus. Une fois que ce corps est une fonction nommée — et uniquement à ce moment-là —, la fermeture restante devient un adaptateur :
static fn (BenchmarkState $value): BenchmarkState => loadPassages($value, $corpus);
L’étape de budget de flow-pipe est la version allégée : fn ($value) => applyBudget($value, $budget).
Tous les wrappers ne sont pas aussi lourds. flow-pipe écrit également fn ($ctx) => $step->apply($ctx). Cette méthode est déjà unaire. Les callables de première classe de PHP 8.1 la suppriment déjà : $step->apply(...).
PFA prend le relais là où FCC s’arrête : un ou plusieurs arguments sont déjà connus. Un appel contenant ? ou ... ne s’exécute pas. Il renvoie une Closure.
$load = loadPassages(?, $corpus);
$trim = applyBudget(?, 8);
Réflexion sur la version 8.6.0beta2 :
load PFA : Closure static (BenchmarkState $state) : BenchmarkState
trim PFA : Closure static (string $text) : string
Ce sont les mêmes structures que les adaptateurs fléchés, sans redéclaration des types. foo(...) est la syntaxe des callables de première classe de PHP 8.1 ; c’est désormais le cas dégénéré de cette même fonctionnalité.
Je n’irais pas jusqu’à parler de révolution. Il s’agit de l’adaptateur que vous écriviez déjà, les types étant conservés dans la fonction d’origine. La différence de temps entre les flèches et les PFA sur la version 8.6.0beta2 se mesurait en quelques nanosecondes. La boucle du pilote de Flow domine toujours ce micro-benchmark. Les PFA sont un gain de lisibilité, pas de performances.
3. ? et ... ne sont pas la même chose
C’est la partie où l’on se trompe facilement, notamment en se contentant de lire le corps du RFC v2 sans aller plus loin. Un amendement a été intégré à la version 8.6 : chaque ? est obligatoire dans la fermeture résultante, même si le paramètre d’origine avait une valeur par défaut. ... conserve quant à lui son caractère facultatif.
$question = exampleOptional(?, ?);
// arité requise : 2. $c reçoit toujours sa valeur par défaut.
$ellipsis = exampleOptional('foo', ...);
// arité requise : 0. $ellipsis() utilise les deux valeurs par défaut.
Les placeholders nommés réorganisent la fermeture, et non l’appel sous-jacent. Deux restrictions modifient la manière dont vous écrivez les pipelines.
Les arguments liés sont évalués au moment de la création, et non lors de l’appel. Une fonction fléchée retarde l’appel interne ; ce n’est pas le cas de PFA.
new ne peut pas faire l’objet d’une utilisation partielle. new stdClass(?) génère une erreur « Cannot create Closure for new expression ». Les fabriques statiques fonctionnent correctement.
Le piège lié aux callbacks est celui déjà décrit dans la RFC :
intval(?)('10', 2); // 10 — l’argument supplémentaire est ignoré
intval(...)('10', 2); // 2 — 2 est devenu $base
array_find() et ses dérivés transmettent une clé en tant que deuxième argument. Préférez intval(?) sauf si vous avez l’intention de transmettre le reste.
4. Intégrer PFA dans Flow
Je n’ai pas modifié le code source de Flow. J’ai simplement intégré les partielles.
Auparavant, la taxe restante était encore une flèche :
$flow = new Flow(
static fn (BenchmarkState $state): BenchmarkState => loadPassages($state, $corpus),
driver: new FiberDriver(),
);
$flow->fn(
static fn (BenchmarkState $state): BenchmarkState => applyBudgetToState($state, 12),
);
Après :
$flow = new Flow(loadPassages(?, $corpus), driver: new FiberDriver());
$flow
->fn(applyBudgetToState(?, 12))
->fn(collect(?, $box));
$flow(new Ip(new BenchmarkState()));
$flow->await();
Il s’agit du code présent dans examples/C-flow-pfa.php. Le résultat obtenu avec la version 8.6.0beta2 était un BenchmarkState chargé et budgétisé. collect(?, $box) correspond au modèle FlowCollector extrait sous forme de fonction. PFA n’a pas inventé de collecteur ; il a supprimé l’adaptateur qui l’entourait.
Rien n’a changé pour Flow. C’est là le résultat intéressant. PFA renvoie déjà la Closure attendue par la méthode fn() de Flow. Pas d’aide partial(). Pas d’API curry. Aucune raison de relever le seuil php: >=8.5 de Flow. Élargir Closure à un callable générique reste une mauvaise idée : les tableaux callables PHP entrent en conflit avec la sémantique existante des tableaux/configurations de Flow.
PHP gère la syntaxe et la liaison des arguments. Flow gère l’exécution.
5. Le pipe n’est pas une application partielle
L’opérateur pipe a été introduit dans la version 8.5. Son opérande de droite doit être un callable à paramètre unique. C’est pourquoi les pipelines de la version 8.5 étaient remplis de flèches entre parenthèses. L’application partielle (PFA) est la moitié manquante de cette phrase, et non un substitut à celle-ci.
Même entrée, deux environnements d’exécution. PHP natif :
$input
|> removeNoise(...)
|> normalizeWhitespace(...)
|> applyBudget(?, 14);
Flow : les trois mêmes fonctions appelables plus collect(?, $box), FiberDriver, await(). Les deux ont produit 'hello world'.
PFA → lier les arguments
|> → acheminer une valeur
Flow → exécuter les tâches
Ils se recoupent au niveau de la composition des fonctions unaires. Ils ne se recoupent pas en ce qui concerne la planification multi-IP, les pilotes, les tâches d’erreur ou les événements. La fonction fn() de Flow correspond à une composition, et non à un opérateur de pipeline. L’utilisation des pipelines se fait au niveau des consommateurs. Si vous n’avez pas besoin de cette boucle, vous n’avez pas besoin de Flow pour la chaîne.
6. La règle « les primitives d’abord »
Nous avons évité d’ajouter partial() à Flow car PHP résout déjà ce problème.
Il s’agit d’une règle d’ingénierie plus générale que ne l’admet généralement un article sur les fonctionnalités d’un langage :
Privilégiez les primitives du langage avant d’inventer l’abstraction du framework.
Quelles autres abstractions d’outils pouvons-nous éviter ?
7. Une expérience sur les outils de vitesse en PHP
La phrase utile n’est pas « PHP est plus rapide qu’un autre langage ». C’est celle-ci :
Choisissez la représentation la plus légère qui contienne tout de même les informations dont votre outil a besoin.
Trouver des fonctions fléchées qui sont déjà PFA ou FCC est un problème lexical. PHP expose déjà un tokeniseur. Un AST nous fournirait une portée et des types dont nous n’avons pas besoin pour affirmer que « cette fn est un appel unique avec un argument restant ». La question de savoir si les tokens sont plus rapides qu’un AST relève d’une hypothèse. Nous n’avons pas mesuré de jumeau.
8. tools/pfa-opportunities
Le PFA Opportunity Scanner, qui adopte une approche prudente, recherche ces fonctions fléchées.
Darkwood conserve les petits outils d’ingénierie locaux au dépôt sous tools/. Il s’agit d’une convention propre à cette expérience, et non d’une affirmation concernant PHP en général. Un petit problème d’ingénierie ne nécessite pas automatiquement un paquet, un framework ou une application distincte. Parfois, la bonne solution se trouve juste à côté du code qu’elle analyse.
tools/
└── pfa-opportunities/
├── bin/scan.php
├── src/functions.php
└── fixtures/
Fichiers PHP
↓
PhpToken::tokenize()
↓
supprimer les tokens ignorables
↓
trouver des fonctions fléchées simples
↓
classer la forme des callables
↓
PFA / FCC / ignorer
L'outil utilise PhpToken::tokenize() car isIgnorable() est une fonction native et chaque token connaît déjà sa ligne. Pas de parseur PHP. Pas de Symfony Finder. RecursiveDirectoryIterator suffit. Rapport uniquement. Environ 540 lignes. Les faux négatifs ne posent pas de problème. Une suggestion PFA faussement positive ne l’est pas.
Les formes à haute confiance :
fn ($x) => foo($x, $bound); // PFA → foo(?, $bound)
fn ($x) => $step->apply($x); // FCC → $step->apply(...)
La suggestion PFA honnête proposée par l’outil lui-même correspond à une fonction à deux arguments. Le code d’exécution simple est un foreach :
$config = new ScanConfig();
foreach ($files as $file) {
foreach (scanFile($file, $config) as $finding) {
echo formatFinding($finding, displayPath($finding->file, $roots)), "\n";
}
}
PFA lie la configuration une seule fois : $scan = scanFile(?, $config);.
9. Le scanner ne détecte pratiquement aucun PFA
Sur nolife-language, flow-pipe et le répertoire src/ de Flow :
144 fichiers
498 107 octets
51 663 tokens significatifs
Candidats PFA 0
Candidats FCC 3
Flow src 0
Zéro est un résultat, pas un échec. Nous avons développé un outil pour tester une hypothèse, et cet outil a indiqué que les arbres de production sélectionnés ne contiennent actuellement aucun site de migration PFA évident. Les tâches Flow officielles sont des corps réels. La fermeture use ($corpus) de LanguagePipelineFactory est également un corps réel.
Les trois occurrences FCC confirment la première étude :
TokenPipelineFlowRunner:$first->apply(...),$step->apply(...)CompressChunkStep:$this->applyToChunk(...)
Les deux occurrences de PFA n’apparaissent que lorsque l’on analyse le fichier « playground » qui a délibérément extrait loadPassages et applyBudget.
10. Les tokens suffisent… jusqu’à ce qu’ils ne suffisent plus
Les tokens nous fournissent la syntaxe. Ils ne nous apportent pas comme par magie une compréhension sémantique.
Le scanner rejette les constructions dont il ne peut pas garantir la sécurité. fn ($p) => $p->toArray() ressemble à un FCC jusqu’à ce que l’on remarque que le récepteur est le paramètre. Il n’y a pas de $p au niveau de array_map sur lequel écrire $p->toArray(...). Suggérer aveuglément PFA ou FCC à cet endroit reviendrait à mentir.
À noter également :
- les prédicats de propriété (
fn ($result) => $result->regression) - les expressions booléennes (
fn ($value) => complicated($value) && other($value)) - les appels imbriqués (
fn ($x) => foo(bar($x), $c)) $xutilisé deux fois, ou pas en premier- les corps de
functionà plusieurs instructions - les références, les restes variadiques,
new, l’identitéfn ($x) => $x - tout ce qui nécessite des types ou une résolution de noms pour être sûr
Si le problème évolue vers une réécriture automatisée fiable, un AST et des outils tenant compte des types pourraient devenir l’abstraction adéquate. Les outils rapides consistent à éviter les mécanismes inutiles, et non à refuser les mécanismes lorsqu’ils deviennent nécessaires.
11. L’outil doit-il utiliser Flow ?
La même fonction scanFile() peut être une tâche Flow, car c’est précisément à cela que sert PFA :
$flow = (new Flow(scanFile(?, $config), driver: new FiberDriver()))
->fn(appendScanFindings(?, $bag));
foreach ($files as $file) {
$flow(new Ip($file));
}
$flow->await();
Les deux exécuteurs ont trouvé les trois mêmes sites FCC. Huit itérations sur la version 8.6.0beta2 :
PHP simple ~79,5 ms
Flow ~120,2 ms
Mémoire maximale ~6 Mio pour les deux
Je ne vais pas dire que « Flow est lent ». Pour un petit balayage de répertoire local dépendant du processeur, Flow ajoute un travail d’orchestration sans résoudre de problème d’orchestration. foreach l’emporte ici. Pas encore.
12. Quand Flow prend tout son sens
Les primitives PHP gèrent déjà la liaison d’arguments, les callables, la syntaxe de pipeline, la tokenisation et les itérations simples.
Flow devient utile lorsque c’est l’exécution qui pose problème :
IP multiples
exécution contrôlée par un pilote
tâches d’erreur
événements
stratégies d’IP
await()
tâches qui se chevauchent lorsque le pilote sélectionné le prend en charge
Ce sont là des capacités vérifiées dans le code source de Flow. La bibliothèque ne dispose pas d’opérateur de branchement/rejonction intégré, ni de module de réessai, ni de module de traçage au-delà des événements Symfony. Je ne vais pas présenter ces fonctionnalités comme si elles étaient déjà disponibles.
Tant que le balayage se résume à « parcourir ces fichiers », foreach reste la primitive.
13. Priorité aux primitives PHP
L’outil le plus rapide n’est pas nécessairement celui écrit dans un autre langage ou s’appuyant sur l’analyseur syntaxique le plus sophistiqué. Parfois, c’est l’outil qui en fait le moins.
PFA au lieu d’une abstraction d’adaptateur.
PhpToken au lieu d’une pile d’analyseurs.
foreach au lieu d’un moteur d’orchestration.
Et lorsque foreach ne suffit plus, c’est là que Flow devient intéressant.
PHP 8.5 nous a apporté le pipe. PHP 8.6 nous apporte l’application partielle. PHP dispose également d’un tokeniseur depuis des années. La question intéressante n’est plus de savoir combien de syntaxe une bibliothèque peut fournir, mais le minimum qu’elle doit fournir avant que la primitive suivante n’échoue réellement.
Priorité aux primitives PHP. N’ajoutez une abstraction d’outillage que lorsque le problème l’exige. Ajoutez Flow lorsque l’exécution devient de l’orchestration.
Dépôt de démonstration
Le terrain de jeu est le dépôt d’étude Symfony flow-partial-function-application. Les scripts se trouvent dans examples/. Le scanner d’opportunités PFA se trouve dans tools/pfa-opportunities/. Flow est une dépendance de chemin et n’a pas été modifié.
docker compose run --rm php86 php examples/run-all.php
docker compose run --rm php86 php tools/pfa-opportunities/bin/scan.php \
/work/content/nolife-language/src \
/work/content/flow-pipe/src \
/work/darkwood/src/Darkwood/Component/Flow
PHP 8.6.0beta2 (php:8.6-rc-cli). Flow 8.1.x. Le PHP 8.5 de l'hôte ne parvient pas à analyser les exemples PFA.
Sources
- PHP 8.6 : https://www.php.net/archive/2026.php#2026-09-10-1
- Code source : https://github.com/matyo91/flow-partial-function-application
- Diapositives : https://github.com/matyo91/slidewire