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.