PHP 8.5 recently landed, so I wanted to try out some of the new stuff. Today I'm diving into the pipe operator (|>).
What's the Pipe Operator?
The pipe operator takes the return value from the left side and passes it to the right side, creating a left-to-right pipeline. Fans say it makes code easier to read and helps you follow the data as it moves along. Sounds cool, right? But does it actually help? Let's see.
I've seen many posts praising this as a game-changing feature. Honestly, my first thought was "Huh, that looks weird." I stayed skeptical. Instead of just going with my gut, I figured I'd throw it into some real code and see what happens.
If you want to dig deeper, check out PHP.Watch Documentation, The Official RFC, or Amit Merchant's examples.
Testing in Practice
Instead of just reading about it, I crafted some practical examples that represent actual production code. I ran benchmarks inside a Laravel 12 app with PHP 8.5. Full code of the command is on GitHub if you want to see everything. Here's what I tried:
1. User Input Sanitization
Traditional:
private function sanitizeEmailTraditional(string $email): string
{
$cleaned = trim($email);
$cleaned = strtolower($cleaned);
$cleaned = filter_var($cleaned, FILTER_SANITIZE_EMAIL);
return $cleaned;
}
With pipe:
private function sanitizeEmailPipe(string $email): string
{
return $email
|> trim(...)
|> strtolower(...)
|> (fn($e) => filter_var($e, FILTER_SANITIZE_EMAIL));
}
Honestly, this is a nice fit. Chaining a few simple functions with the pipe operator feels smooth and the code stays readable.
2. CSV Data Processing
Say you need to filter, calculate, sort, and format some CSV data.
Traditional:
private function processEmployeesTraditional(array $employees): array
{
$engineering = array_filter(
$employees,
fn($emp) => $emp['department'] === 'Engineering'
);
$withBonus = array_map(function ($emp) {
$emp['salary'] = (int) $emp['salary'];
$emp['bonus'] = (int) ($emp['salary'] * 0.1);
return $emp;
}, $engineering);
usort($withBonus, fn($a, $b) => $b['salary'] <=> $a['salary']);
return array_map(
fn($emp) => [
'name' => $emp['name'],
'total' => $emp['salary'] + $emp['bonus']
],
$withBonus
);
}
With pipe:
private function processEmployeesPipe(array $employees): array
{
return $employees
|> (fn($e) => array_filter($e, fn($emp) => $emp['department'] === 'Engineering'))
|> (fn($e) => array_map(function ($emp) {
$emp['salary'] = (int) $emp['salary'];
$emp['bonus'] = (int) ($emp['salary'] * 0.1);
return $emp;
}, $e))
|> (fn($e) => (function ($arr) {
usort($arr, fn($a, $b) => $b['salary'] <=> $a['salary']);
return $arr;
})($e))
|> (fn($e) => array_map(
fn($emp) => [
'name' => $emp['name'],
'total' => $emp['salary'] + $emp['bonus']
], $e
));
}
Notice how we needed nested closures and an awkward wrapper just to handle usort. The "natural flow" actually gets messier here.
3. API Response Formatting
Turning raw data from a third-party api into something usable.
Traditional:
private function formatApiResponseTraditional(array $raw): array
{
$metadata = json_decode($raw['metadata'], true);
$formatted = [
'userId' => $raw['user_id'],
'userName' => $raw['user_name'],
'createdAt' => $raw['created_at'],
];
$formatted['createdAt'] = strtotime($formatted['createdAt']);
$formatted['tags'] = explode(',', $raw['tags']);
$formatted['settings'] = $metadata;
return $formatted;
}
With pipe:
private function formatApiResponsePipe(array $raw): array
{
return $raw
|> (fn($data) => json_decode($data['metadata'], true))
|> (fn($metadata) => [
'userId' => $raw['user_id'],
'userName' => $raw['user_name'],
'createdAt' => $raw['created_at'],
'tags' => $raw['tags'],
'metadata' => $metadata,
])
|> (fn($data) => [
...$data,
'createdAt' => strtotime($data['createdAt']),
])
|> (fn($data) => [
...$data,
'tags' => explode(',', $data['tags']),
])
|> (fn($data) => [
'userId' => $data['userId'],
'userName' => $data['userName'],
'createdAt' => $data['createdAt'],
'tags' => $data['tags'],
'settings' => $data['metadata'],
]);
}
What could be a clear, single transformation gets split into five steps, each making a new array. Is this really easier to maintain? I don't think so. The traditional code shows exactly what's going on.
Performance Results
I ran each benchmark million times. Here's what happened:
User input sanitization:
Traditional: 0.000462 ms
Pipe: 0.000562 ms
Traditional is 21.55% faster
CSV data processing:
Traditional: 0.002088 ms
Pipe: 0.002702 ms
Traditional is 29.40% faster
API response formatting:
Traditional: 0.001432 ms
Pipe: 0.002064 ms
Traditional is 44.15% faster
Performance hit lands somewhere between 20% and 45%. In absolute terms, we're talking microseconds, so for most web requests, it won't matter. But the pipe operator adds overhead without bringing performance gains.
When Pipes Actually Shine
In some cases, pipes do work well:
// Simple, pure fn chains
return $price
|> applyDiscount(...)
|> addTax(...)
|> formatCurrency(...);
// Clean str transformations
return $input
|> trim(...)
|> strtolower(...)
|> ucfirst(...);
They're great for lining up a few pure functions, each step obvious and named. But the second you need to mutate data, deal with side effects, or handle functions that don't fit the pipeline pattern (like usort), the traditional way comes out ahead.
My Take
The pipe operator isn't the readability miracle people make it out to be. For short, pure chains, it's fine. But for real-world code with mutations or tricky logic, the traditional approach is clearer, easier to maintain, and actually faster.
Don't refactor working code just to use pipes. If your team isn't already comfortable with this functional style, adding pipes will probably confuse more than it helps.
The performance cost is tiny and won't matter for most apps. The real issue is readability in complex code, and that's where pipes stumble. After trying these examples, I won't be reaching for pipes in production. The old way works better for the kind of code I write.
Should You Use It?
If you love chaining pure functions and the left-to-right style clicks for you, go for it. But for more complicated stuff or if your team's not into functional programming just stick with what you know. It's easier to read, easier to maintain, and just makes more sense despite what the pipe advocates claim.
Author's Note
Thanks for sticking around!
Find me on dev.to, linkedin, or you can check out my work on github.
Notes from real-world Laravel.
Merry Christmas and Happy New Year! Don't forget to upgrade to PHP 8.5 and explore the new features. 🎄

Top comments (7)
While I agree you shouldn't use the pipe operator if it is not a natural fit. I do think the examples are contrived.
The CSV example can be done with two function calls, instead of four.
No need to use the pipe operator for two function calls.
For the API response formatting, I don't think anyone is going to use the pipe operator because it is not a chain of functions that are executed.
I think where the pipe operator will shine is as a replacement of the fluent API. Instead of binding methods to a certain type of object, the functions are standalone. Which makes them less prone to side effects. And it will be easier to add your own functions.
Thanks for the feedback and mostly agree.
My take was to show that this operator is fully suitable for all cases how many says.
The examples are intentionally simple to make the data flow obvious.
In real code, your
array_reduce + usortsolution is perfectly valid and probably clearer today.I also agree the API response example isn't a great fit, but I've seen many similar examples using it that way, for example:
amitmerchant.com/seven-realworld-e...
Pipes are a tool, not replacement for existing patterns.
In the linked article the logging and metrics enrichment example is just insane.
An
array_mergefor each added key is just not knowing how the function works.Also the
redactSensitivefunction is weird because the function needs to know all the keys that need to be redacted.True it is not a replacement, that was bad wording.
But the fact that a pipe guides you to composition and immutability makes it easier to write better code.
Also a pipe can change the output type after each function call. While with a fluent API the type stays the same, only at the end it can be converted to a different type.
It is a more powerful tool than the fluent API pattern.
Definitely found the second and mostly the third example outworldly. The original example is already needlessly repetitive and shouldn't pass a code review. And, as the previous comment said, it's indeed not a fit for pipe because.... You're just building an array. So it doesn't even fit as a "bad example for pipe usage", it's not really an example of what anyone would do.
And receiving such article in PHP Weekly doesn't help either.
Cool, thanks for the feedback!
Totally agree, this is not the kind of code I'd ever want to see in the real code review 🙂
The example is intentionally a bit "outworldly", because I keep running into very similar beautiful-but-questionable pipe-heavy snippets on twiter and linkedin every day.
The goal was more "look, this exists in the wild" than "please do this at work". I'll try to make that intent clearer next time.
How is it beautiful code?
How does questionable fit into beautiful? It is not like a song with slurs in the lyrics.
I think we are on the same page. But it feels like the communication is giving mixed signals.
Fair point! That's on me for not being explicit enough.
When I said "beautiful-but-questionable," I meant esthetically appealing pipe-heavy snippets I keep seeing shared in the internet, not code I consider good or recommend writing.
The intent was to critique that trend, not to present those examples as realistic or desirable. 🙂
Appreciate the pushback, this helped highlight where the communication could been clearer.