PHP 8.5 adds native pipe operator, but array_map still requires manual adjustment
PHP 8.5, available since November 2025, introduced the |> operator for chaining data transformations. Not every array_* function adapts directly: the pipe only passes one argument, and that changes how you refactor legacy pipelines.

PHP 8.5 adds native pipe operator, but array_map still requires manual adjustment
Editor's note: the source consulted for this article (the official PHP manual on PHP 8.5's new features) does not include the version's release date. The claim that PHP 8.5 has been available since November 2025 could not be confirmed with this source and should be read with that caveat.
PHP 8.5, available since November 2025, introduced the |> operator for chaining data transformations. Not every array_* function adapts directly: the pipe only passes one argument, and that changes how you refactor legacy pipelines.
PHP 8.5, available since November 2025, introduced the |> operator for chaining data transformations. Not every array_* function adapts directly: the pipe only passes one argument, and that changes how you refactor legacy pipelines.
PHP 8.5 introduced the |> operator for chaining data transformations. Not every array_* function adapts directly: the pipe only passes one argument, and that changes how you refactor legacy pipelines.
The pipeline every PHP monolith accumulates
If you maintain a PHP monolith with a few years on the road, you've already run into this pattern: a function that takes a list, normalizes it, filters it, and transforms it again, all chained together in nested calls to array_map() and array_filter(). It works, but it reads from the inside out, and each level of indentation loses a bit more of the original intent.
function getActiveUserEmails(array $users): array
{
return array_map(
fn ($user) => strtolower($user['email']),
array_filter(
array_map('normalizeUser', $users),
fn ($user) => $user['active'] === true
)
);
}To understand this function you have to read it from the inside out: first array_map('normalizeUser', ...), then array_filter, and only then the outer array_map. It's the kind of code every team refactors at some point, usually into named intermediate variables. PHP 8.5 offers a third way: the pipe operator (|>).

How the pipe operator actually works
The PHP manual sums up the operator with a minimal example:
$result = "Hello World" |> strlen(...);
print $result . PHP_EOL; // Prints "11"$value |> callable(...) is syntactic sugar for callable($value). The left side becomes the argument, and only the argument, of the right side. This rule is the entire point of this article: the pipe is strictly unary. It knows nothing about the signature of the function you're calling, it just pushes a value into it.
This works well for functions that expect exactly one required argument: trim(...), strlen(...), json_decode(...) (the remaining parameters are optional), or an instance method that only needs the implicit $this plus one value. For anything that requires two or more arguments, like most functions in the array_* family, the pipe alone doesn't solve it.
Refactoring the pipeline, step by step
The first step, before even thinking about |>, is simply naming each stage with a variable. This already improves readability without touching any new syntax:
function getActiveUserEmails(array $users): array
{
$normalized = array_map('normalizeUser', $users);
$active = array_filter($normalized, fn ($user) => $user['active'] === true);
$emails = array_map(fn ($user) => strtolower($user['email']), $active);
return $emails;
}Now the version with pipe. Notice that each stage still receives a closure that accepts a single parameter, the array coming from the previous stage, and it's that closure, not array_map() or array_filter() directly, that goes into the pipe:
function getActiveUserEmails(array $users): array
{
return $users
|> fn (array $u) => array_map(normalizeUser(...), $u)
|> fn (array $u) => array_filter($u, fn ($user) => $user['active'] === true)
|> fn (array $u) => array_map(fn ($user) => strtolower($user['email']), $u);
}The gain here isn't magic: we traded indentation for a top-to-bottom reading, in the order the transformations actually happen. For someone reading the code for the first time, that already helps a lot, especially in pipelines with four or five stages, common in import and ETL routines inside monoliths.
Where the pipe doesn't work
The temptation, after seeing the example above, is to try to shorten it even further and pipe straight into array_map(...):
// Don't do this: array_map() expects the callback as the first
// argument, not the array. The pipe delivers $users in that position.
$result = $users |> array_map(...); // TypeError at runtimeThe problem is parameter order. array_filter(array $array, ?callable $callback = null) takes the array first, so in simple cases (without a custom callback) you can even pipe straight into it. array_map(?callable $callback, array $array, array ...$arrays), on the other hand, takes the callback first: the value the pipe delivers lands in the wrong position. That's why the wrapping closures in the examples above aren't a stylistic choice, they're necessary.
The same limitation shows up with instance methods that need context beyond the value being transformed:
final class PriceFormatter
{
public function normalize(float $amount): float { /* ... */ }
public function convert(float $amount, string $targetCurrency): float { /* ... */ }
}
$formatter = new PriceFormatter();
// Works: normalize() only expects the value coming from the pipe
$normalized = $rawAmount |> $formatter->normalize(...);
// Doesn't work: convert() expects a second argument
$converted = $rawAmount |> $formatter->convert(...); // missing $targetCurrency
// Correct way: close over the second argument in a closure
$converted = $rawAmount |> fn (float $amount) => $formatter->convert($amount, 'BRL');PHP has no native partial application, so any function, static method, or instance method that needs more than one argument will require a wrapping closure in the middle of the pipe. In pipelines with many configuration parameters (currency, locale, time zone), this can end up canceling out the readability gain, because you're back to having one closure per stage, just now with |> between them instead of nested parentheses.
Adjusting CI and lint for the new syntax
Before spreading |> across the monolith, two points in the build pipeline need attention. The first is the PHP version used in CI:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.5'
tools: phpstan, php-cs-fixer
- run: composer install --no-interaction
- run: vendor/bin/phpstan analyse
- run: vendor/bin/php-cs-fixer fix --dry-runAnd composer.json needs to declare the minimum requirement, so that Composer rejects installation in older environments:
{
"require": {
"php": "^8.5"
}
}The second point is less obvious: static analyzers and formatters (PHPStan, Psalm, PHP-CS-Fixer, PHP_CodeSniffer) only understand |> if their internal parser already supports PHP 8.5. Before enabling the syntax in any shared repository, confirm the installed version of these tools in your pipeline. If the parser is outdated, the lint won't flag a style issue, it will simply fail with an unknown token error, breaking the entire build for a reason nobody on the team will immediately understand.
When it's worth migrating, and when it isn't
The pipe operator helps in linear transformation pipelines: raw data goes in, a series of steps happens, a result comes out. In monoliths with ETL routines, API payload normalization, or report formatting, that's exactly the shape.
There are situations where I wouldn't migrate:
- Libraries shared across services with different PHP versions. If an internal package still needs to run on services stuck on PHP 8.2 or 8.3, introducing
|>breaks the entire file's parsing on those versions, it's not a feature that fails gracefully. - Pipelines with heavy context injection per stage. If each step needs two or three extra arguments, the wrapping closures take away much of the operator's visual benefit, and the traditional nested
array_map/array_filtercan still be more straightforward to debug. - Teams that still do a lot of line-by-line breakpoint debugging. A long chain of
|>concentrates several calls into a single expression, which makes it harder to set a breakpoint midway without breaking the expression back into variables.
Outside those cases, it's worth trading nesting for pipe in new routines and, gradually, in the most frequently read parts of the legacy codebase. As with any syntax change in a monolith, the practical rule is: refactor where readability actually improves, not just where the code gets shorter.
Translated from the Brazilian Portuguese original · Read the original
PHP 8.5 Adds Native Pipe Operator: What Changes for Those Maintaining a Monolith in Production
Available since November 2025, the |> operator replaces nested chains with a top-to-bottom reading. But it only solves part of the readability problem that exists in older PHP codebases.





