Home

New in PHP 8.5: The Pipe Operator

New in PHP 8.5: The Pipe Operator
Liked 3 times

One of the most exciting additions in PHP 8.5 is the pipe operator. It enables more readable and expressive code when working with nested function calls.

One of PHP's longstanding limitations is that scalar values (strings, integers, arrays, etc.) cannot have methods. As a result, deeply nested operations often end up looking cluttered and hard to read:

echo ucfirst(strtolower(preg_replace('@\s+@', '', "HELLO THERE!")));

There are a few workarounds, such as using intermediate variables:

$temp = "HELLO THERE!";
$temp = preg_replace('@\s+@', '', $temp);
$temp = strtolower($temp);
$temp = ucfirst($temp);

echo $temp;

Or using indentation to improve readability:

echo ucfirst(
    strtolower(
        preg_replace(
            '@\s+@',
            '',
            "HELLO THERE!"
        ),
    ),
);

These approaches work, but they're clunky. The new pipe operator offers a more elegant solution by allowing you to chain the result of the left-hand expression into a callable on the right:

echo "HELLO THERE!"
    |> fn (string $str) => preg_replace('@\s+@', '', $str)
    |> strtolower(...)
    |> ucfirst(...)
;

If you've ever worked with fluent setters, this pattern should feel very familiar.

How It Works

The pipe operator passes the result of the left-hand expression as an argument to the callable on the right. That callable must accept exactly one required parameter. It pairs perfectly with first-class callables (ucfirst(...)) and short arrow functions (fn($x) => $x).

It’s a full expression, so you can use it wherever any expression is allowed—assignments, return statements, conditionals, etc.

Operator Precedence

The pipe operator is left-associative, just like most arithmetic operators. That means expressions are evaluated from left to right:

// correctly evaluates to 2
$result = 2 + 2 |> sqrt(...);

// equivalent to this expression with parentheses
$result = (2 + 2) |> sqrt(...);

You can, of course, use parentheses to alter evaluation order:

// will evaluate to something like 3.4142135623731
$result = 2 + (2 |> sqrt(...));

// equivalent to
$result = 2 + sqrt(2);

The pipe operator has higher precedence than comparison operators, but lower than arithmetic ones. So:

$result = 2 + 2 |> sqrt(...) > 5;
// is equivalent to
$result = ((2 + 2) |> sqrt(...)) > 5
// is equivalent to
$result = sqrt(2 + 2) > 5;

The rules are intuitive, but one case where parentheses are often necessary is with the null coalescing operator:

$result = 5 |> trueOrNullFunction(...) ?? false;
// equivalent to
$result = (5 |> trueOrNullFunction(...)) ?? false;
// equivalent to
$result = trueOrNullFunction(5) ?? false;

And if you're providing an optional callable, parentheses are required:

// this is wrong without parentheses
$result = 5 |> $possiblyNullCallable ?? fn ($x) => true;
// this is correct
$result = 5 |> ($possiblyNullCallable ?? fn ($x) => true);

Higher-Order Functions

The pipe operator really shines when used with higher-order functions—functions that return other functions:

function map(callable $mapper): Closure
{
    return fn (array $array) => array_map($mapper, $array);
}

function filter(callable $filter): Closure
{
    return fn (array $array) => array_filter($array, $filter);
}

// assume is_odd and pow2 exist
$result = [1, 2, 3, 4, 5]
    |> filter(is_odd(...))
    |> map(pow2(...))
;

Caveats

The pipe operator is highly optimized and introduces virtually no overhead compared to traditional function calls. However, it does have some limitations:

  • It only works with callables that accept exactly one required argument.
  • Callables that require additional arguments must be wrapped in a closure.
$result = [1, 2, 3]
    |> fn (array $array) => array_filter($array, fn (int $num) => $num % 2 === 0)
    |> fn (array $array) => array_map(fn (int $num) => $num ** 2, $array)
;

This adds a tiny performance cost, though it’s usually negligible. If the Partial Function Application RFC is accepted, it will make this kind of usage even cleaner.

p>One major limitation is that the pipe operator doesn’t support references. For example, the following code will throw an error:

 

function square(int &$number): void
{
    $number **= 2;
}

$num = 2;
$num |> square(...); // ❌ This will fail

Some functions in PHP can accept both references and values, depending on how they're called (something I didn't even know was possible before writing this article). These will work with the pipe operator, but the values will always be passed by value, not by reference.

Conclusion

The new pipe operator is a welcome addition to PHP 8.5 that makes functional-style programming much cleaner and more readable. Whether you're cleaning up strings, transforming arrays, or composing complex logic, it allows you to express intent without sacrificing clarity.

While it has a few limitations—such as lack of reference support and the need for wrappers around multi-argument functions—these are relatively minor compared to the readability gains. And with future features like partial function application potentially on the way, the story will only get better.

What do you think? Will the pipe operator find a place in your workflow, or do you prefer more traditional patterns?


More from New in PHP 8.5:


Tip: To comment on or like this post, open it on your favourite Fediverse instance, such as Mastodon or Lemmy.
© 2024 Dominik Chrástecký. All rights reserved.