PHP 8.5 Pipe Operator: Clean Function Chaining in One Line

Chain PHP functions left-to-right with PHP 8.5's new pipe operator. No more nested calls or temp vars. With real examples, gotchas, and when to reach for it.

Steven Richardson
Steven Richardson
· 4 min read

You've written trim(strtolower(strip_tags($input))) more times than you'd like to admit. It works, but it reads right-to-left — the opposite of how the transformation actually flows. PHP 8.5 ships the |> pipe operator to fix that.

PHP 8.5 Pipe Operator Syntax#

The operator passes the value on the left as the first argument to the callable on the right:

$result = $input |> trim(...) |> strtolower(...);

The (...) syntax is PHP's first-class callable syntax — it creates a closure from the function reference so the pipe has something to invoke. Without it you'd be handing the pipe a string like "trim", which won't fly here.

Chain as many callables as you like:

$slug = ' PHP 8.5 Released '
    |> trim(...)
    |> strtolower(...)
    |> (fn(string $str) => str_replace(' ', '-', $str));
// "php-8.5-released"

Notice the arrow function is wrapped in parentheses. That's required — without them the arrow function's body captures everything to the end of the expression and PHP throws a parse error. Always wrap fn() steps.

Before and After#

A real example: sanitising user input before storing it.

Before — nested:

$clean = trim(strtolower(strip_tags($userInput)));

Reads inside-out. You have to scan inward to find where the transformation starts.

Before — intermediate variables:

$stripped = strip_tags($userInput);
$lower    = strtolower($stripped);
$clean    = trim($lower);

Readable, but the variable names are throwaway noise when all you care about is the final result.

After — php function chaining with the pipe operator:

$clean = $userInput
    |> strip_tags(...)
    |> strtolower(...)
    |> trim(...);

Left-to-right. Each step is explicit. No intermediary names to invent.

Using Arrow Functions in the PHP 8.5 Pipe Operator#

When a function requires more than one argument, wrap it in an arrow function:

$price = $rawInput
    |> (fn(string $v) => (float) $v)               // cast string to float
    |> (fn(float $v) => round($v, 2))              // round to 2 decimal places
    |> (fn(float $v) => number_format($v, 2));     // format as currency string
// "12.50"

I prefer extracting closures when the pipeline gets long — it keeps each step readable without going full named function:

$toFloat = fn(string $v) => (float) $v;
$round   = fn(float $v)  => round($v, 2);
$format  = fn(float $v)  => number_format($v, 2);

$price = $rawInput |> $toFloat |> $round |> $format;

Static methods and instance methods work too:

$result = $value
    |> MyTransformer::sanitise(...)
    |> $formatter->format(...);

Gotchas and Edge Cases#

Single argument only. The pipe always passes one value, period. Functions that require multiple arguments — str_replace, substr, preg_replace — must be wrapped in a closure. There is no partial application syntax in PHP 8.5. It may arrive in 8.6.

Arrow functions need parentheses. Without them you get a parse error:

// Bad — parse error, arrow fn captures the rest of the expression
$result = $value |> fn($v) => strtolower($v) |> trim(...);

// Good
$result = $value |> (fn($v) => strtolower($v)) |> trim(...);

Void callables produce null. If any step in the chain has a void return type, the next callable receives null. Don't pipe through side-effect functions (logging, dispatching events) mid-chain.

No short-circuit. The pipe operator doesn't stop on null or false. If you need null-safe handling, you still need the nullsafe operator ?-> or an explicit guard closure.

PHP 8.5 only. Using |> on PHP 8.4 or earlier is a parse error. Add a platform constraint to composer.json if you ship this to a shared codebase:

{
    "require": {
        "php": ">=8.5"
    }
}

Not method chaining. The pipe operator passes values through callables. It's not the same as fluent method chaining on an object ($query->where()->orderBy()). They solve similar readability problems in different contexts.

Wrapping Up#

Reach for |> when you have a linear transformation sequence and want the code to read in execution order. For longer pipelines where many steps need extra arguments, a dedicated package like league/pipeline is still worth a look — the closure wrapping can get noisy at scale. But for everyday string and value transformations, the PHP 8.5 pipe operator is a clean, zero-dependency fit.

The pipe operator pairs especially well with PHP readonly classes as value objects — you can pipe raw primitives through a transformation chain and produce a fully-validated, immutable value object at the end. For the broader picture of modern PHP language features and where they fit in a production Laravel stack, The Complete Laravel Developer Toolchain for 2026 covers PHP 8.4 and 8.5 features alongside deployment, testing, and monitoring tooling.

FAQ#

Can I pipe through instance methods?

Yes, use the first-class callable syntax: $value |> $formatter->format(...). Static methods work the same way: $value |> MyClass::staticMethod(...).

What if I need partial application for a function with multiple arguments?

PHP 8.5 doesn't have partial application syntax. Wrap the function in a closure: |> (fn($v) => str_replace(' ', '-', $v)). This is verbose but explicit.

How do I handle null in the middle of a pipe?

The pipe operator doesn't short-circuit on null or false. If you need null-safe handling, add a guard closure: |> (fn($v) => $v === null ? null : transform($v)). Or use the nullsafe operator outside the pipe if it makes sense.

Is this the same as object method chaining?

No, method chaining works on objects that return $this (fluent interfaces): $query->where()->orderBy(). The pipe operator chains values through functions. They solve similar readability problems in different contexts.

Steven Richardson
Steven Richardson

CTO at Digitonic. Writing about Laravel, architecture, and the craft of leading software teams from the west coast of Scotland.