skip to content

In PHP 8.5, what does the |> pipe operator do, and what must the callable on its right-hand side satisfy?

level: middleimportance: should knowfreq 40%

answer

  1. left value becomes the only argument
  2. left to right, like nested calls reversed
  3. one required parameter, no by-reference
  4. parenthesize arrow functions
  5. binds tighter, == binds looser

basics

~20 s

PHP 8.5's |> passes its left value as the only argument to the callable on its right, so chains read in execution order. That callable must need one argument, not take it by reference, and an arrow function there must be parenthesized.

solid answer

~50 s

`$x |> f(...)` is the same as `f($x)`, and a chain `$title |> trim(...) |> strtolower(...)` means `strtolower(trim($title))`, written in the order it runs. The right-hand side can be any callable expression: a first-class callable, a function-name string, an array callable, an invokable object, a variable holding a `Closure`, or an arrow function **in parentheses**; an unparenthesized `fn` is a compile error. Rules: the callable receives **exactly one argument**, so a function with two required parameters throws `ArgumentCountError`, while extra optional parameters keep their defaults. A **by-reference** parameter throws `Error: ... could not be passed by reference`. A `void` step passes `null` on. Precedence matters: arithmetic binds tighter (`5 + 2 |> f(...)` pipes 7), while `==`, `??` and the ternary bind looser than `|>`. Multi-argument functions are wrapped: `(fn ($s) => str_replace(' ', '-', $s))`.

code

php · 18 lines
php
<?php
declare(strict_types=1);

$raw = '  Hello, PHP 8.5 World!  ';

$slug = $raw
    |> trim(...)
    |> strtolower(...)
    |> (fn (string $s): string => preg_replace('/[^a-z0-9]+/', '-', $s))
    |> (fn (string $s): string => trim($s, '-'));

echo $slug, "\n"; // hello-php-8-5-world

try {
    $raw |> str_replace(...);   // three required parameters
} catch (ArgumentCountError $e) {
    echo $e->getMessage(), "\n"; // str_replace() expects at least 3 arguments, 1 given
}

go deeper

for a junior

Recall that |> passes the left value to a one-argument callable on the right and that chains read in execution order, from PHP 8.5.

for a middle

Explain the one-required-parameter rule, the by-reference restriction, parenthesized arrow functions, void steps and precedence against arithmetic and comparison.

for a senior

Judge when a pipe chain is clearer than temporary variables, and how to wrap multi-argument functions without burying the logic in closures.

for a principal

Decide how a team adopts |> in an 8.5 codebase, including style rules for wrappers and mixing pipes with other operators.

## What the operator does PHP 8.5 added the **pipe operator** `|>`. It takes the value on its left, passes it as the **only argument** to the callable on its right, and evaluates to what that callable returns: ```php $length = 'Hello World' |> strlen(...); // same as strlen('Hello World') $slug = $title |> trim(...) |> strtolower(...); // same as strtolower(trim($title)) ``` The operator is left-associative, so a chain runs step by step from left to right. The benefit is readability: nested calls read inside-out, pipes read in execution order, and there is no need for a temporary variable per step. ## What may stand on the right The right-hand side is **any expression that evaluates to a callable**: | Right-hand side | Example | |---|---| | first-class callable | `\|> trim(...)`, `\|> $slugger->slug(...)` | | function-name string | `\|> 'strtolower'` | | array callable | `\|> [$slugger, 'slug']`, `\|> [Slugger::class, 'clean']` | | invokable object | `\|> new TitleValidator()` | | variable holding a Closure | `\|> $normalize` | | arrow function, parenthesized | `\|> (fn (string $s) => str_replace(' ', '-', $s))` | An arrow function **must** be wrapped in parentheses; otherwise the compiler stops with `Arrow functions on the right hand side of |> must be parenthesized`, because `fn ($x) => $x |> g(...)` would be ambiguous. If the right side evaluates to something that is not callable, PHP throws an `Error`. ## Rules for the callable 1. **One argument is passed.** A callable with **two or more required** parameters fails exactly like a normal call with too few arguments, with an `ArgumentCountError` (`Too few arguments to function ...` for a user function, `... expects at least 3 arguments, 1 given` for a built-in such as `str_replace()`). Additional **optional** parameters are fine and take their defaults, so `trim(...)` works although `trim()` accepts a second argument. 2. **No by-reference parameter.** The piped value is not a variable, so a function declaring `&$param` throws `Error: ... Argument #1 ($param) could not be passed by reference`. `sort(...)` or `array_push(...)` cannot be pipe steps. 3. **Types apply as in any call.** Under `declare(strict_types=1)`, a step returning `int` into a step declaring `string` throws `TypeError`. 4. **A `void` step yields `null`**, which becomes the next step's input; with a string-typed next step this is a `TypeError`. ## Evaluation order PHP evaluates the left operand first, then the right-hand **expression** (which may itself compute which callable to use, such as `(cond() ? f(...) : g(...))`), then calls it. If any step throws, the rest of the chain is skipped and the exception propagates like any other. ## Precedence `|>` sits between arithmetic and comparison: - **tighter than `|>`**: arithmetic and string concatenation. `5 + 2 |> double(...)` pipes `7`. - **looser than `|>`**: comparison, `??` and the ternary. `$n |> isOdd(...) ? 'odd' : 'even'` pipes first, then branches, and `$x |> f(...) == 10` compares the result. When a chain mixes operators, parentheses make intent explicit. ## Pipes versus the alternatives - **Nested calls** such as `slugify(strtolower(trim($raw)))` are equivalent but read inside-out; beyond three levels they are hard to scan. - **Temporary variables** (`$t = trim($raw); $t = strtolower($t);`) read in order and are easy to debug with a breakpoint on each line, at the cost of repeated assignments. - **Pipes** read in order without temporaries, and each step is a callable that can be named, reused and tested. - **Collection pipelines** over arrays, using `array_map()` and `array_filter()`, are a different tool: they transform many elements, while `|>` threads one value through several functions. A pipe step can call such a helper on an array value. ## Adapting multi-argument functions Most string and array functions need more than one argument. Wrap them in a parenthesized arrow function that supplies the rest: ```php $slug = $title |> trim(...) |> strtolower(...) |> (fn (string $s): string => preg_replace('/[^a-z0-9]+/', '-', $s)) |> (fn (string $s): string => trim($s, '-')); ``` There is a cost difference too. When the right-hand side is written directly as `name(...)`, `Class::method(...)` or `$obj->method(...)`, the compiler turns `$x |> name(...)` into the plain call `name($x)`, so no closure is created. An arrow-function wrapper, by contrast, creates a closure object each time the step is evaluated. Long chains of wrappers can also read worse than a few named temporary variables, so choose the pipe when most steps are single-argument callables.

  • In PHP 8.5, why does 42 |> fn ($x) => $x + 1 fail to compile?
    An arrow function's body extends as far right as possible, so an unparenthesized `fn` on the right of `|>` would swallow any following pipe steps into its own body. PHP refuses the ambiguity with `Arrow functions on the right hand side of |> must be parenthesized`; write `42 |> (fn ($x) => $x + 1)`.
  • What does $n |> isOdd(...) ? 'odd' : 'even' evaluate to in PHP 8.5?
    The pipe binds tighter than the ternary, so it is `(isOdd($n)) ? 'odd' : 'even'`: the call runs first and its boolean result selects the branch. Likewise `$x |> f(...) == 10` compares `f($x)` with 10, while `5 + 2 |> f(...)` computes `5 + 2` first and pipes 7.

saying these in an interview costs you the question

  • |> passes the left value as the first of several arguments, like a partial call.
  • An arrow function can follow |> without parentheses.
  • |> works in PHP 8.4 with an experimental flag.
  • A step can modify the piped value in place through a by-reference parameter.
  • 5 + 2 |> f(...) calls f(2) and adds 5.