DEV Community

Cover image for PHP 8.5 Pipe Operator (|>) – Is It Worth Using?
Ivan Mykhavko
Ivan Mykhavko

Posted on

PHP 8.5 Pipe Operator (|>) – Is It Worth Using?

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;
}
Enter fullscreen mode Exit fullscreen mode

With pipe:

private function sanitizeEmailPipe(string $email): string
{
    return $email
            |> trim(...)
            |> strtolower(...)
            |> (fn($e) => filter_var($e, FILTER_SANITIZE_EMAIL));
}
Enter fullscreen mode Exit fullscreen mode

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
    );
}
Enter fullscreen mode Exit fullscreen mode

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
            ));
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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'],
            ]);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Performance Comparison: traditional vs pipe operator

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(...);
Enter fullscreen mode Exit fullscreen mode

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 (0)