PHP’s function-composition story has always been awkward. When you need to pass a value through several transformations, you either write deeply nested calls that read inside-out, or you introduce a chain of intermediate variables that carry no semantic weight.
PHP 8.5 added the pipe operator (|>) to address this. It shipped with the November 2025 release and has been stable in production through the 8.5.x series. Since Laravel 13 requires PHP 8.3 and is fully compatible with 8.5, teams upgrading their runtime are reaching this feature without any additional install step.
The question is not whether to use it. The question is where it helps and where it creates a new variety of hard-to-read code.
What the operator does
The pipe operator takes the value on its left and passes it as the first argument to the callable on its right. A placeholder ($$) lets you specify position when the function signature puts the input value somewhere other than the first parameter.
// Traditional: inside-out, hard to read at a glance
$result = array_values(array_unique(array_map('trim', explode(',', $input))));
// Pipe operator: left-to-right, matches the mental model
$result = $input
|> explode(',', $$)
|> array_map('trim', $$)
|> array_unique($$)
|> array_values($$);
Both are equivalent. The pipe version reads in the order of execution. The $$ placeholder is required because explode takes the delimiter as the first argument and the string as the second.
Request sanitisation pipelines
Laravel request handling is the most natural fit. Input arrives, moves through normalisation, and emerges ready for validation. Each step is a distinct concern, and the pipe operator makes those concerns explicit.
use Illuminate\Http\Request;
final class ContactFormController extends Controller
{
public function store(Request $request): JsonResponse
{
$email = $request->input('email')
|> trim($$)
|> strtolower($$)
|> fn (string $e) => filter_var($e, FILTER_SANITIZE_EMAIL);
$message = $request->input('message')
|> trim($$)
|> strip_tags($$)
|> fn (string $m) => mb_substr($m, 0, 2000);
// Pass clean values to the form data object
ContactFormData::from(email: $email, message: $message)->send();
return response()->json(['status' => 'queued']);
}
}
Each step is one concern. The pipeline reads as a description of what happens to the value, not as a nested expression that requires tracking open parentheses.
Collection processing in service classes
Service classes that build query result pipelines for reporting or export work see immediate benefit. The equivalent without the operator either nests deeply or requires throwaway variable names.
final class ActiveSubscriberReport
{
public function forPeriod(Carbon $from, Carbon $to): array
{
return Subscription::query()
->whereBetween('activated_at', [$from, $to])
->whereNull('cancelled_at')
->get(['customer_id', 'plan', 'mrr_cents'])
->toArray()
|> fn (array $rows) => array_filter($rows, fn ($r) => $r['mrr_cents'] > 0)
|> fn (array $rows) => array_map(
fn ($r) => [...$r, 'mrr' => round($r['mrr_cents'] / 100, 2)],
$rows,
)
|> fn (array $rows) => array_values($rows);
}
}
The Eloquent query half uses method chaining, which is Laravel’s own pipeline pattern. The post-fetch transformation half uses the pipe operator, which takes over once the result is a plain array. The two idioms co-exist without conflict.
Where the operator adds noise
Two patterns produce code that is harder to read after the change, not easier.
Short chains. If a pipeline is two steps, the pipe operator adds visual overhead without improving readability. A single assignment is clearer.
// This is not an improvement
$trimmed = $input |> trim($$);
// This is
$trimmed = trim($input);
Complex anonymous functions as steps. When each pipe step contains a multi-line closure, the pipeline becomes a nested structure with different syntax. Break those steps into named private methods instead.
// Hard to read as a pipeline
$result = $data
|> fn (array $d) => array_filter($d, function ($item) use ($threshold) {
return $item['score'] >= $threshold && !in_array($item['id'], $this->excluded);
})
|> fn (array $d) => array_map(function ($item) {
$item['normalised'] = $this->normalise($item['score']);
return $item;
}, $d);
// Better: named methods make the intent visible
$result = $data
|> fn (array $d) => $this->filterBelowThreshold($d)
|> fn (array $d) => $this->addNormalisedScore($d);
The pipe operator is a composition tool, not an excuse to put logic inside the composition layer.
Pass-by-reference is not supported
One constraint matters when migrating array manipulation code: the pipe operator does not support callables that require pass-by-reference. sort(), usort(), and arsort() modify their argument in place and cannot be used as pipe steps directly. Wrap them in closures that return the sorted result instead.
// sort() passes by reference — this does not work
$sorted = $items |> sort($$); // Error
// Wrap in a closure that returns
$sorted = $items
|> fn (array $a) => (static function (array $arr): array {
sort($arr);
return $arr;
})($$);
// Or just use a named helper
$sorted = $items |> fn (array $a) => $this->sortItems($a);
PHP version gating
Until your entire deployment surface is on PHP 8.5, the pipe operator cannot go into shared library code. Use it in application-layer classes where the runtime is controlled, and guard a require in composer.json if you are extracting any pipeline logic into a package.
{
"require": {
"php": "^8.5"
}
}
A composer check-platform-reqs in CI will catch a version mismatch before it reaches staging.
The honest summary
The pipe operator solves a real problem with PHP’s function composition syntax. In Laravel, the best use cases are request normalisation, post-collection transformation, and service-layer pipelines that move a value through sequential, stateless steps.
It does not replace Laravel’s own Pipeline class for middleware-style patterns with shared context, and it does not improve code that was already clear. The goal is to use it where the nested-call or intermediate-variable alternatives were genuinely harder to follow, and to leave everything else as it was.