The right-hand side of PHP 8.5's new |> operator has to be a callable, not a function call. Everything interesting about the feature hides in that requirement. The operator itself could fit on a sticky note: it evaluates the expression on its left and passes the result as the first argument to whatever sits on its right. So you write $input |> trim(...), leaning on the first-class callable syntax we got back in 8.1. But the moment a step needs a second argument, say a str_replace with a search and a replacement, you must wrap it: (fn($s) => str_replace(' ', '-', $s)), and the outer parentheses are not decoration. Leave them off and the arrow function greedily captures the rest of the chain and the parser gives up on you.
<?php
// PHP 8.5: each step takes a value, returns a value
function sanitizeComment(string $input): string
{
return $input
|> trim(...)
|> strip_tags(...)
|> (fn($s) => htmlspecialchars($s, ENT_QUOTES, 'UTF-8'));
}That wrapper is the complaint I hear most about the current implementation, and yes, PHP 8.6 is slated to bring partial function application so you can pre-fill arguments and drop most of these closures. I hope it takes its time. The verbosity is doing quiet pedagogical work. Every time you type fn($s) =>, the language is asking you a small pointed question: is this step really an anonymous shim, or is it a thing with a name that your codebase will want again next Tuesday?
Because when the steps do get names, the operator suddenly earns its keep. Static methods pipe directly with the callable syntax, so $raw |> Slug::normalize(...) |> Slug::truncate(...) reads top to bottom with zero ceremony. Instance methods don't get that luxury, you still have to wrap $formatter->format() in a closure, which is a genuine wart, but it also means piped code drifts toward small stateless functions with one obvious input. That drift is free at runtime: pipe chains compile down to opcodes roughly equivalent to the nested calls you'd have written anyway, so there is no performance bill for factoring your pipeline into a dozen tiny named pieces.
Now the honest part. The strongest case against bothering with |> is that PHP already had a perfectly serviceable answer: intermediate variables. $trimmed, $slugified, $lowercased. Those names document intent, they show up in your debugger, you can var_dump any of them without ceremony. A pipe chain gives you none of that, and it adds a trap the old style never had: a void callable evaluates to null, so if you sling var_dump into the middle of a chain to take a quick look, the next step receives null instead of your value and the pipeline quietly poisons everything downstream. No exception, no warning, just wrong output three steps later. Add the deployment reality, this is new syntax that will not parse on 8.4 or anything older, so your whole fleet and your CI runners need to be on 8.5 first, and skepticism looks pretty reasonable.
I still land on using it, and the var_dump trap is part of why. A pipe chain makes a promise the variable version never quite states: every step takes a value and returns a value, nothing else happens in here. Side effects don't fit the shape, and in code review that mismatch is visible from across the room. When a colleague tries to smuggle a logger call into the middle of a transformation, the chain itself objects. With four temp variables, that same logger call slides in between assignments and nobody blinks. The operator doesn't let you do anything new. It changes what looks wrong, and after enough years of maintaining other people's sanitization code, I've come to value that more than debugger convenience.
So my working rule since 8.5 landed: pipes for anything that is honestly a transformation, slug builders, input sanitizers, the filter-map-unique-values dance on arrays. Plain old variables for anything with branching, early returns, or heavy use of object state, where a chain would just be a costume. Tooling won't fight you, PhpStorm and Intelephense in VS Code both understand the syntax as of early 2026. And if a chain accumulates more than two inline closures, I stop and extract named functions before merging, because that chain is telling me the domain has vocabulary I haven't written down yet.
The Elixir crowd has had |> since before some of our legacy code was legacy, and F# before that, so PHP is late to this particular party. Fine. We arrived with opcache-level optimization and first-class callables already in the toolbox, which is a decent way to show up late. My open question is about 8.6: once partial application lands and the (fn($s) => ...) wrappers evaporate, will you keep extracting named steps, or was the parenthesis tax the only thing keeping your pipelines honest? Put differently, how many inline closures do you allow in a chain before you block the merge request? I say two. Tell me why I'm wrong.




Comments
No comments yet — be the first.
Open the discussion
No account or password needed — just enter your e-mail and we’ll send you a one-time sign-in link. First time here? You’re set up automatically.
Your rating will be applied automatically after you sign in.
Check your inbox
We’ve sent a sign-in link to …. Open it on this device — this tab will sign you in automatically.
Nothing arrived? Check your spam folder — and mark the mail as "Not spam" so it lands in your inbox next time.