PHP 8.5 Adds Pipe Operator: What it means
The upcoming PHP 8.5 release brings a highly anticipated feature that has been years in the making: the pipe operator (|>). This small but powerful addition promises to revolutionize how we write and compose functions in PHP.
The pipe operator, spelled |>, is deceptively simple. It takes the value on its left side and passes it as the single argument to a function or callable on its right side:
function square($n) { return $n * $n; }$numbers = [1, 2, 3]; $squares = array_map(function ($x) use ($square) { return $square($x); }, $numbers); // Not necessary with pipes!
But where it becomes interesting is when it's repeated or chained to form a "pipeline." For example:
function doubleAndAddOne($n) { return $n * 2 + 1; }$numbers = [1, 2, 3]; $doublesAndOnes = array_map(function ($x) use (doubleAndAddOne) { return doubleAndAddOne($x); }, $numbers); // No temp variables or ugly nesting needed!
Anyone who has worked on the Unix/Linux command line will likely recognize the similarity to the shell pipe, |. That's very deliberate, as it is effectively the same thing: use the output from the left side as the input on the right side.
The pipe operator appears in many languages, mostly in the functional world. F# has essentially the exact same operator, as does OCaml. Elixir has a slightly fancier version (which we considered but ultimately decided against for now). Numerous PHP libraries exist in the wild that offer similar capability with many extra expensive steps, including my own Crell/fp.
The story of pipes in PHP begins with Hack/HHVM, Facebook's PHP fork née competitive implementation. Hack included many features beyond what PHP 5 of the day offered; many of them eventually ended up in later PHP versions. One of its features was a unique spin on a pipe operator.
function doubleAndAddOne($n) { return $n * 2 + 1; }$numbers = [1, 2, 3]; $result = array_map(function ($x) use ($doubleAndAddOne, $square) { return $doubleAndAddOne($square($x)); }, $numbers);
This was powerful but also somewhat limiting. It was very non-standard, unlike any other language. It also meant a weird, one-off syntax for partially-calling functions that worked only when paired with pipes.
That's when I, fresh off of writing a book on functional programming in PHP that talked about function composition, decided to take a swing at it. In particular, I partnered with a team to work on Partial Function Application (PFA) as a separate RFC from a more traditional pipe. The idea was that turning a multi-parameter function into the single-parameter function that |> needed was a useful feature on its own, and should be usable elsewhere.
The syntax was a bit different than the Hack version, in order to make it more flexible: some_function(?, 5, ?, 3, ...), which would take a 5-or-more parameter function and turn it into a 3 parameter function. Sadly, PFA didn't pass due to some engine complexity issues, and that largely undermined the v2 Pipe RFC, too.
However, we did get a consolation prize out of it: First Class Callables (the array_values(...) syntax), courtesy Nikita Popov, were by design a "junior", degenerate version of partial function application.
Fast-forward to 2025, and I was sufficiently bored to take another swing at pipes. This time with a better implementation with lots of hand-holding from Ilija Tovilo and Arnaud Le Blanc, both part of the PHP Foundation dev team, I was able to get it through.
The |> operator appears in many languages, mostly in the functional world. F# has essentially the exact same operator, as does OCaml. Elixir has a slightly fancier version (which we considered but ultimately decided against for now). Numerous PHP libraries exist in the wild that offer similar capability with many extra expensive steps, including my own Crell/fp.
The implementation itself is almost trivial; it's just syntax sugar for the temp variable version, effectively. However, the best features are the ones that can combine with others or be used in novel ways to punch above their weight.
We saw above how a long array manipulation process could now be condensed into a single chained expression. Now imagine using that in places where only a single expression is allowed, such as a match():
match ($numbers[0], $numbers[1]) { [1, 2] => 'first', [3, 4] => 'second', }
Or consider that the right-side can also be a function call that returns a Closure:
function makeDouble($n) { return function ($x) use ($square) { return $square($x * 2); }; }$numbers = [1, 2, 3]; $doubles = array_map(function ($x) use (makeDouble) { return makeDouble($x)(doubleAndAddOne); }, $numbers); // Not necessary with pipes!
That means with a few functions that return functions: Which... gives us mostly the same thing as the long-discussed scalar methods! Only pipes are more flexible as you can use any function on the right-side, not just those that have been blessed by the language designers as methods.
At this point, pipe comes very close to being "extension functions," a feature of Kotlin and C# that allows writing functions that look like methods on an object, but are actually just stand-alone functions. It's spelled a bit differently (| instead of -), but it's 75% of the way there, for free.
What if some steps in the pipe may return null? We can, with a single function, "lift" the elements of our chain to handle null values in the same fashion as null-safe methods. That's right, we just implemented a Maybe Monad with a pipe and a single-line function.
The potential is absolutely huge. I don't think it's immodest to say that the pipe operator has one of the highest "bangs for the buck" of any feature in recent memory, alongside such niceties as constructor property promotion. And all thanks to a little syntax sugar.
Although pipes are a major milestone, we're not done. There is active work on not one but two follow-up RFCs. The first is a second attempt at Partial Function Application. This is a larger feature, but with first-class callables already bringing in much of the necessary plumbing, which simplifies the implementation.
The second is a function composition operator. Where pipe executes immediately, function composition creates a new function by sticking two functions end-to-end. That would mean the streams example above could be further optimized by combining the map() calls:
function compose($f, $g) { return function ($x) use ($g, $f) { return $f($g($x)); }; }$numbers = [1, 2, 3]; $doublesAndOnes = array_map(function ($x) use (compose, doubleAndAddOne) { return compose(doubleAndAddOne, square)(doubleAndAddOne); }, $numbers); // This stream example could be optimized!
This one is definitely not going to make it into PHP 8.5, but I am hopeful that we'll be able to get it into 8.6. Stay tuned.
Special thanks to Arnaud Le Blanc from the PHP Foundation team for picking it up to update the implementation. If you’d like to help push PHP forward, consider becoming a sponsor.