The PHP 8.5 Pipe Operator
PHP 8.5's pipe operator (|>) is here, and it's quietly transforming how we write data transformation chains.
PHP 8.5 shipped with a feature that functional programming enthusiasts have been requesting for years: the pipe operator (|>). If you’ve used pipes in Unix shells, Elixir, F#, or even JavaScript proposals, the concept will feel immediately familiar. If you haven’t, you’re about to discover one of the cleanest ways to express data transformations in PHP.
What the Pipe Operator Does
The pipe operator takes the result of the expression on its left side and passes it as the first argument to the function on its right side. That’s it. One simple rule that unlocks remarkably readable code.
Here’s the basic syntax:
$result = $value |> 'trim' |> 'strtolower' |> 'ucfirst';
This is equivalent to:
$result = ucfirst(strtolower(trim($value)));
Both produce the same output, but the piped version reads left to right — the same direction you naturally read English. The nested version forces you to read inside out, which gets progressively harder as the chain grows.
Why Nested Calls Get Painful
Let’s look at a real-world example. Say you’re processing user input from a form — a common task in any PHP application:
// Without pipes: read inside-out
$slug = strtolower(
trim(
preg_replace('/[^a-zA-Z0-9\s-]/', '',
str_replace(' ', '-',
strip_tags($input)
)
)
)
);
Quick — what happens first? You have to start from the innermost call (strip_tags) and work outward. Now here’s the same logic with the pipe operator:
// With pipes: read top to bottom
$slug = $input
|> 'strip_tags'
|> fn($s) => str_replace(' ', '-', $s)
|> fn($s) => preg_replace('/[^a-zA-Z0-9\s-]/', '', $s)
|> 'trim'
|> 'strtolower';
Each step is clear, ordered, and easy to follow. You can read it like a recipe: take the input, strip HTML tags, replace spaces with dashes, remove special characters, trim whitespace, and lowercase everything. The mental overhead drops dramatically.
Using Pipes With Closures and Arrow Functions
The pipe operator works with any callable, not just named functions. This is where it gets really practical, because most real-world transformations need additional arguments:
$price = $rawPrice
|> fn($p) => round($p, 2)
|> fn($p) => max($p, 0)
|> fn($p) => number_format($p, 2, '.', ',');
Arrow functions (fn()) are your best friend here. They keep each step concise while letting you pass the piped value into any position of the called function.
Practical Patterns for Laravel and Symfony Developers
If you’re working in a framework, the pipe operator fits naturally into data processing pipelines. Here are some patterns you might recognize:
Request Data Sanitization
$cleanEmail = $request->input('email')
|> 'trim'
|> 'strtolower'
|> fn($e) => filter_var($e, FILTER_SANITIZE_EMAIL);
Building API Responses
$response = $queryResults
|> fn($data) => array_map(fn($item) => $item->toArray(), $data)
|> fn($data) => array_filter($data, fn($item) => $item['active'])
|> fn($data) => array_values($data)
|> 'json_encode';
Configuration Processing
$dbHost = getenv('DATABASE_HOST')
|> fn($h) => $h ?: 'localhost'
|> 'trim'
|> fn($h) => strtolower($h);
What the Pipe Operator Is NOT
It’s worth being clear about the boundaries. The pipe operator is syntactic sugar for function composition. It doesn’t create lazy evaluation, it doesn’t enable method chaining on objects, and it’s not a replacement for Laravel’s Collection pipeline or Symfony’s Workflow component.
If you’re already using collect($items)->map()->filter()->values() in Laravel, keep doing that. Collections have their own pipeline mechanism that works beautifully. The pipe operator shines in the spaces between — processing scalar values, cleaning strings, transforming configuration data, and composing standalone functions.
Performance Considerations
The pipe operator compiles down to nested function calls at the engine level. There’s no runtime overhead beyond what you’d get from writing the equivalent nested syntax. It’s purely a readability improvement, which is exactly what you want from syntactic sugar — zero cost at runtime, significant payoff in code clarity.
Combining Pipes With Other PHP 8.x Features
The pipe operator pairs well with other recent PHP additions:
// With named arguments in the closure
$result = $input
|> fn($v) => mb_convert_encoding($v, to_encoding: 'UTF-8', from_encoding: 'ISO-8859-1')
|> 'trim'
|> fn($v) => mb_strtolower($v, encoding: 'UTF-8');
You can also combine pipes with match expressions, null coalescing, and first-class callables for expressive one-liners that remain readable.
Should You Start Using It?
Yes, but thoughtfully. Like any syntactic feature, the pipe operator is best when it improves clarity. A two-step transformation probably doesn’t need it. A five-step chain absolutely benefits from it. Use it where the left-to-right flow makes the code’s intent obvious at a glance.
If you’re on PHP 8.5 — and if you’re not, it’s worth the upgrade for this and many other improvements — start looking for those deeply nested function calls in your codebase. They’re prime candidates for pipe operator refactoring, and your future self (and your teammates) will thank you for the readability boost.
The pipe operator isn’t a revolutionary feature. It’s something better: a practical one. It takes code you’re already writing and makes it clearer, one |> at a time.