Eine Leserin hat mir letzte Woche einen Diff geschickt, eine Zeile rot eingekreist: `$tag = sprintf('[%s] %s', currentRequestId(), ?);`. Ihre Frage: „Ist das dasselbe wie die Arrow Function, die es ersetzt hat?“ Ist es nicht, und diese eine Zeile verrät dir das meiste, was du über Partial Function Application in PHP 8.6 wissen musst. Ich mag das Feature sehr und will es ab dem ersten Tag in unseren Codebases haben. Ich glaube aber auch, dass die meisten von uns es die ersten sechs Monate falsch lesen werden, weil es aussieht wie Syntaxzucker für `fn` und sich verhält wie ein Konstruktor, der einen Schnappschuss macht.

Kurz zur Erinnerung für alle, die die RFC-Threads übersprungen haben. PHP 8.6 ist in der Beta, das Release ist für etwa den 19. November 2026 geplant. PFA baut auf der First-Class-Callable-Syntax `trim(...)` auf, die wir mit 8.1 bekommen haben. Du rufst eine Funktion oder Methode ganz normal auf, setzt aber ein `?` in jeden Slot, den du später füllen willst, und bekommst ein Callable zurück. Ein einzelnes `?` steht für genau ein Argument. Ein abschließendes `...` steht für alle restlichen Argumente und muss ganz am Ende stehen, nach den positionalen und den benannten Slots. Schreibst du `str_replace(' ', '-', ?)`, hast du einen Slugifier ohne Closure, ohne einen Parameternamen, den du dir ausdenken musstest, und ohne doppeltes `$value`. Das klappt auch mit statischen und Instanzmethoden und verträgt sich gut mit benannten Argumenten, sodass sich `Book::create(category: Category::THRILLER, title: ?)` fast wie Konfiguration liest.

Das ist die fehlende Hälfte des Pipe-Operators. Seit es `|>` gibt, hatte jede Pipeline, die ich geschrieben habe, ein hässliches Glied: die geklammerte Arrow Function, die du brauchst, sobald ein Schritt mehr als ein Argument nimmt. Fünf saubere Stufen und dann ein `(fn ($v) => str_replace(' ', '-', $v))` mittendrin wie ein Kabelbinder. Mit PFA wird dieser Schritt zu `str_replace(' ', '-', ?)`, und das `?` zeigt genau, wo der Wert hineinfließt. Go-Leute erzählen uns gern, dass explizite Schleifen cleveres Verketten schlagen, und ganz unrecht haben sie nicht. Aber sobald eine Sprache eine Pipe hat, braucht die Pipe genau das, und 8.6 liefert es.

Jetzt zurück zum roten Kreis. Ein Partial wertet jedes Argument, das kein Platzhalter ist, in dem Moment aus, in dem du es erzeugst. Der Wert wird eingefangen und bei jedem Aufruf wiederverwendet. `currentRequestId()` lief in der Zeile meiner Leserin also genau einmal, als der Worker hochfuhr und seinen Formatter baute. Die Arrow Function davor hat die Funktion bei jeder Logzeile aufgerufen. In einem klassischen FPM-Request merkt das niemand, weil Partial und Request zusammen sterben. In einem langlebigen Swoole-, RoadRunner- oder Queue-Worker trägt jede Logzeile der nächsten vierzigtausend Jobs die ID des ersten Jobs. Deine Rufbereitschaft merkt das um 2 Uhr nachts, beim Greppen nach einem Request, der scheinbar den gesamten Traffic der Nacht abgewickelt hat.

Der stärkste Einwand verdient es, ernst genommen zu werden. Man könnte sagen, das beweise, dass PFA eine Falle ist, dass `?` nur ein weiteres Sigil in einer Sprache ist, die davon schon reichlich hat, und dass eine explizite Closure ehrlicher ist, weil sie genau zeigt, was wann läuft. Ich stimme halb zu. Eine Closure macht das Timing offensichtlich, ein Partial versteckt es. Aber das sofortige Einfangen ist die richtige Semantik, und oft ist es genau das, was du willst: das teure Ding einmal bauen, viele Male aufrufen. Die Closure-Variante, die `getPrefix()` bei jedem Aufruf neu ausführt, hat ihre eigenen Bugs, die sehen nur anders aus. Was ich aus dem Einwand mitnehme: Wir brauchen eine Review-Gewohnheit. Verbieten muss niemand die Syntax.

Die zweite Falle ist leiser. Ein Partial ist eine brandneue Closure. Es zeigt nicht einfach auf die ursprüngliche Funktion, so wie `strlen(...)` das tut. Eigene Attribute am Original werden nicht übernommen. Die Ausnahmen sind `#[\NoDiscard]` und `#[\SensitiveParameter]`, und Letzteres überlebt nur an Parametern, die du offen gelassen hast. Wenn dein Framework per Reflection auf Callables schaut, um Routen-Metadaten, Validierungsregeln oder Event-Listener-Marker zu finden, rutscht ein Partial still daran vorbei. Teams mit attributgesteuertem Container-Wiring sollten danach greppen, bevor sie irgendwem erlauben, Partials an die Registry zu übergeben.

Hier also die Hausregel, die ich meinem eigenen Team vorschlage. Ein `?` oder ein abschließendes `...` ist überall in Ordnung, wo die festen Argumente Literale, Konstanten, Enum-Cases oder Werte sind, die schon in lokalen Variablen liegen. Sobald ein festes Argument ein Funktionsaufruf, ein Methodenaufruf oder irgendetwas ist, das die Uhr, den Request oder den Container liest, zieht die Autorin oder der Autor es entweder in eine benannte Variable eine Zeile darüber, damit das Einfangen sichtbar wird, oder schreibt bewusst eine Closure. Partials landen nie dort, wo Reflection Attribute liest. Das kostet uns ab und zu eine zusätzliche Zeile, und dafür muss niemand raten, wann ein Argument ausgewertet wird.

Ich bin mir aber nicht sicher, ob die Regel stimmt. Für Pipelines, in denen fast jedes feste Argument sowieso ein Literal ist, ist sie vielleicht zu vorsichtig. Für langlebige Worker ist sie vielleicht zu locker. Also sag mir in den Kommentaren: Wenn 8.6 im November erscheint, wird dein Team Funktionsaufrufe als feste Argumente in einem Partial erlauben, oder verlangt ihr, dass eingefangene Werte zuerst in eine Variable gezogen werden?