PHP 8.5 Pipe Operator: Clean Function Chaining in One Line

3 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.

php
php-8.5
functional-programming
Steven Richardson

Steven is a software engineer with a passion for building scalable web applications. He enjoys sharing his knowledge through articles and tutorials.