Finally! We are popping the Dom Pérignon, tearing up the confetti, and ritually burning our procedural programming textbooks. PHP 8.5 brings the legendary Pipe Operator |>. The holy grail for everyone who prays to the functional programming gods at night and secretly envies the hipsters in Elixir or F#.

No more nesting functions inside each other like a Russian Matryoshka doll. No more helper variables like $tmp1, $tmp2, $tmp47. We are writing data flow from left to right, exactly the way we naturally think!

The PHP marketing department would put this on their slides:

$result = " Hello " |> trim(...) |> strtoupper(...);

“Wow! Such purity! Such elegance!” the crowd screams, throwing their bras onto the stage.

But then you wake up and realize reality is a different beast entirely. Welcome to the hell of parentheses, anonymous functions, and performance masochism. Let's look at why this new feature is about as useful as a waterproof tea bag.

Case Study: How to write the same thing, but more complicated

Imagine a classic scenario: You want to normalize text for English subtitles. The process: trim whitespace, split into words, capitalize the first letter of each word, and glue it back together.

Compare for yourself:

// Traditional nesting - unreadable horror (Matryoshka style)
$result = implode(' ', array_map(ucfirst(...), preg_split('/\s+/', trim($input))));

// With the Pipe Operator - WOW! 🎉
$result = $input
    |> trim(...)
    |> preg_split('/\s+/', ...)
    |> array_map(ucfirst(...), ...)
    |> implode(' ', ...);

At first glance, it’s a Zen garden. You see data flowing from top to bottom like a waterfall. Your brain purrs with bliss because it doesn't have to decipher parentheses from the inside out. It's like reading a recipe: “Take input, trim it, split into words, adjust, glue.” Beautiful. Feng-shui in practice.

EXCEPT THAT’S NOT HOW IT WORKS!

That beautiful example above is a dirty syntactic lie. The PHP parser would choke and die screaming SYNTAX ERROR if it tried to process that code.

Let's explain why right now.

How it was done in the past

We’ve already seen the unreadable nesting horror. Instead, let's look at the “peasant” style we all secretly use when no one is looking:

// Traditional way with helper variables - clear, functional, boring (110 chars)
$_ = trim($input);                             // remove whitespace
$_ = preg_split('/\s+/', $_);                  // split into words
$_ = array_map(ucfirst(...), $_);              // capitalize letters
$processed = implode(' ', $_);                 // join back

Clear, readable. Every junior dev knows what's happening. The helper variable $_ won't win a beauty contest, but it works, costs nothing, and doesn't get in the way.

By the way, this code has 110 characters. Remember that number.

First Slap: ... is not Partial Application

The ... placeholder ONLY works when you are piping into the first and simultaneously the only parameter!

So while trim(...) is fine, anything more complex hits a brick wall. PHP (unlike languages that do it properly) can't say “here is a function and here is the hole for the argument.”

And because most functions in PHP have a parameter order chosen by a random number generator (the needle/haystack chaos), you have to use arrow functions. Get your fingers ready, you’ll be typing a lot of fn, $_, and arrows:

$processed = $input
    |> trim(...)                               // The only moment it works nicely
    |> fn($_) => preg_split('/\s+/', $_)       // Arrow func. Variable.
    |> fn($_) => array_map(ucfirst(...), $_)   // Nested arrow func, yum.
    |> fn($_) => implode(' ', $_);

Second Slap: Parenthesis Hell

You run it, expecting applause. Instead, the PHP interpreter spits out:
Fatal error: Arrow functions on the right hand side of |> must be parenthesized.

Excuse me? I have to put arrow functions in parentheses? Why? Because of operator precedence. The parser is as confused as a headless chicken, so you have to explicitly wrap every arrow function so it understands where one pipe ends and the next begins.

So your “elegant” code now looks like this:

$processed = $input
    |> trim(...)
    |> (fn($_) => preg_split('/\s+/', $_))     // Parenthesis. Arrow func. Variable. Parenthesis.
    |> (fn($_) => array_map(ucfirst(...), $_)) // More parentheses...
    |> (fn($_) => implode(' ', $_));           // Why are we doing this to ourselves?

Congratulations! Your code is now:

  • 53% longer than the helper variable variant (now has 169 characters).
  • Visually resembles a lobotomized LISP (nothing but parentheses).
  • Carries the overhead of creating a closure at every single step.

You can't stop progress!

K.O.: References? Forget about it

This is the part where laughter turns into tears and gnashing of teeth. Imagine you want to replace something in the text and you're interested in how many times the replacement happened (the &$count parameter in str_replace).

In the classic “outdated” code, you would simply pass the variable by reference.

$_ = str_replace('!', '', $_, count: $count); // $count happily increments

But inside arrow functions within the pipe? Tough luck. Arrow functions (fn) in PHP capture variables from the outer scope by value (copy).

Look at this bear trap:

$count = 0;

// We expect $count to increase...
$processed = $input
    |> (fn($_) => str_replace('!', '', $_, count: $count)) // Betrayal!
    |> trim(...)
    ...

echo $count; // The result is always 0. Zero. Zilch.

What happened?

Nothing. No error. No Notice. No Warning. PHP simply silently took a copy of zero, sent it into the function, the function happily incremented it inside its own little bubble, and then threw it in the trash. Your original variable $count remained untouched.

If you want to fix this, you have to use the old syntax function($_) use (&$count) { return ... }, thereby losing the last shreds of dignity and elegance.

What happens under the hood? (Spoiler: Nothing pretty)

You might be thinking: “Okay, it's ugly, but surely it's a super-fast optimized macro, right?”

(No sane person thinks this, but let's pretend.)

The Pipe Operator is not a smart macro that rewrites (fn($_) => ...) code into simple calls. Every single step really creates a new instance of a Closure object. For every operation. For every line.

So instead of a simple function call, PHP internally does this:

  1. Create a Closure object.
  2. Call it.
  3. Throw it away (and let the Garbage Collector have a seizure).
  4. Repeat for the next line.

It is like ordering an Uber to go to the fridge for a beer. You get there, but it is unnecessarily expensive, takes longer, and the neighbors will think things about you.

To Typehint, or Not to Typehint?

This is the Hamlet-esque question of modern PHP. In a chain of pipe operators, you have to decide:

Option A: I am an honest masochist and I write (fn(string $_): array => ...).

Result: The code is so long it doesn't fit on two monitors side-by-side. You will wear your hands down to stumps and your colleagues will hate you during every Code Review.

Option B: I am a lazy punk and I just write (fn($_) => ...).

Result: Your IDE stops hinting and strictly configured static analysis (PHPStan) starts screaming “Mixed type everywhere!”

It is a choice between carpal tunnel syndrome and coding blind.

Verdict

The PHP 8.5 Pipe Operator is like that expensive designer juicer you bought in January during a fit of healthy lifestyle enthusiasm. It looks nice on the counter, you tell everyone about it, but when you actually have to use it, you realize that cleaning the mesh sieve takes three times longer than just eating the whole orange, peel and all.

The Pipe Operator only makes sense if:

  1. All functions take exactly one parameter. ✅
  2. Or you don't care about performance. ✅
  3. Or you really love parentheses. ✅

So… it's great for tutorials and conferences!

Recommendation: Stick to helper variables. They are cheap, they work, they don't block references, and you don't have to write (fn($_) => ...) ten times in a row.


P.S.: PHP 8.6 will solve this with Partial Function Application. But taking comfort in that now is like telling a hungry man: ‘Hang in there, next year the schnitzel will actually have meat.’