Partial Function Application: The PHP 8.6 Feature That Completes the Pipe Operator
PHP 8.6's partial function application lets you pre-fill arguments with a ? placeholder, and it finally makes the 8.5 pipe operator genuinely useful.
Every so often a language feature lands that does not just add a tool but quietly rearranges how you think about the tools you already have. Partial function application, accepted for PHP 8.6 by a unanimous 33 to 0 vote, is one of those. On its own it is a tidy bit of syntax sugar. Paired with the pipe operator we got in PHP 8.5, it turns a slightly awkward feature into something I expect to reach for constantly. PHP 8.6 is scheduled for release around November 19, 2026, so this is still a few months out, but it is worth understanding now.
Starting With What We Already Have
To see why partial application matters, it helps to recall first-class callables, added back in PHP 8.1. That feature let you grab a reference to a function using the ... notation:
$replace = str_replace(...);
// Call it later
$replace([' ', '_'], '-', $title);
This shines when you hand a function to another function:
$trimmed = array_map(trim(...), $strings);
Under the hood, that reference is just a closure wrapping the original function. The shortcoming is that first-class callables capture the function as a whole. You cannot fill in some of the arguments up front. For a three-argument function like str_replace(), that limitation bites quickly. If you wanted a reusable slug helper, you were back to writing the closure out by hand:
$slugReplace = fn (string $input): string => str_replace([' ', '_'], '-', $input);
Enter the Placeholder
Partial function application, or PFA, fixes exactly that. You write a normal-looking function call but leave one or more arguments as a ? placeholder. PHP hands you back a new closure that waits for the missing values:
$slugReplace = str_replace([' ', '_'], '-', ?);
$slugReplace('Hello World'); // "Hello-World"
That single ? becomes one parameter on the resulting closure. The mental model is simple: each placeholder is a blank you promise to fill later, in the order they appear.
You are not limited to one placeholder, and they do not have to sit at the end of the argument list:
$replaceWhitespace = str_replace(' ', ?, ?);
$replaceWhitespace('=', $input); // replacement, then subject
$filterNumbers = array_filter(?, is_numeric(...));
$filterNumbers(['a', 1, 'b', 2]); // [1, 2]
That second example is the kind of thing I find genuinely pleasant. The input array is the blank, and the callback is supplied immediately using a first-class callable. The two features compose naturally.
The Variadic Placeholder
Alongside ?, PFA introduces a ... variadic placeholder that captures zero or more remaining parameters at once:
$replaceWhitespace = str_replace(' ', ...);
$replaceWhitespace('=', $input);
Because the variadic placeholder means zero or more, it also gives you a clean way to defer execution of a fully specified call. You bind all the arguments now but only invoke when you decide to:
$performQuery = query('UPDATE table SET ...', ...);
if ($needToUpdate) {
$performQuery();
}
That is handy for expensive operations you only want to run conditionally, without dragging a bulky closure around.
It Works With Named Arguments, Objects, and Methods
PFA understands named arguments, and the variadic placeholder is smart about the leftovers. This example reorders parameters so the mode becomes the first argument of the new closure:
$filterNumbers = array_filter(
mode: ?,
callable: is_numeric(...),
... // captures the remaining $array parameter
);
$filterNumbers(ARRAY_FILTER_USE_KEY, ['a', 1, 'b', 2]);
It also works on static methods and instance methods, which is where I suspect a lot of real application code will use it:
$makeThriller = Book::make(
title: ?,
category: Category::THRILLER,
);
$book = $makeThriller(title: 'Timeline Taxi');
$publish = $book->publish(DateTime::now(), ...);
if ($shouldPublish) {
$publish();
}
It even works through magic __call() methods. There are a handful of places where PFA is not allowed: constructor calls, and the functions compact(), extract(), func_get_args(), and get_defined_vars(). Those depend on the calling scope or argument introspection in ways that do not survive being wrapped in a closure. Everywhere else, it just works.
Why This Makes the Pipe Operator Click
Here is the part I care about most. PHP 8.5 gave us the pipe operator, |>, which feeds a value through a chain of single-argument functions. The catch is right there in the description: each step must take exactly one parameter. Any function that needs more arguments had to be wrapped in a closure, which cluttered the chain:
$output = $input
|> trim(...)
|> (fn (string $s) => str_replace(' ', '-', $s))
|> (fn (string $s) => str_replace(['.', '/'], '', $s))
|> strtolower(...);
With partial application, every one of those multi-argument steps collapses into a placeholder call. The pipeline reads as a clean sequence of transformations:
$output = $input
|> trim(...)
|> str_replace(' ', '-', ?)
|> str_replace(['.', '/'], '', ?)
|> strtolower(...);
The ? marks where the piped value lands at each stage. This is the combination that makes functional-style PHP feel native rather than bolted on. The pipe operator provides the flow, and partial application provides the shaped functions to flow through it.
Should You Care Now?
PFA will not land until PHP 8.6 in November, so you cannot ship it today. But it is accepted and the vote was unanimous, which is about as solid as a future feature gets in PHP internals. If you have started experimenting with the 8.5 pipe operator and found yourself writing throwaway closures to bridge the single-argument gap, this is the answer to that friction.
For those of us who have lived through a decade of PHP slowly absorbing functional ideas, partial application feels like a capstone. It is small in surface area, the error messages are reportedly clear when you misuse it, and it composes beautifully with features we already have. I would not rewrite anything for it, but I will be reaching for it the day 8.6 hits my machines.