Development

PHP 8.5's Pipe Operator: Finally, Readable Function Chains That Make Sense.

6 minutes

Right, I'm going to be honest with you - when I first heard about PHP 8.5's pipe operator, my initial reaction was "oh great, another operator to remember." and I can't see me needing to use this. But after spending some time playing with it I've completely changed my tune. This little |> operator might just be the best quality-of-life improvement PHP has seen since constructor property promotion.

And here's the thing - this isn't some newfangled concept. The pipe operator has been around since the 1960s when Doug McIlroy introduced it to Unix systems. Languages like F#, Elixir, and OCaml have had it for years. We're not breaking new ground here; we're finally catching up. And that's a good thing.

Let me show you why our team at Jump24 is genuinely excited about this one.

The Problem We've All Faced

You know that feeling when you're writing PHP and you end up with code that looks like this?

php
1<?php
2 
3// The nested nightmare we've all written
4$result = array_values(
5 array_unique(
6 array_filter(
7 array_map(
8 'strtolower',
9 explode(' ', trim($input))
10 ),
11 fn($word) => strlen($word) > 3
12 )
13 )
14);

Yeah, we've all been there. Reading that code is like reading a book backwards - you have to start from the innermost function and work your way out. It's the PHP equivalent of those Russian nesting dolls, except less charming and more likely to give you a headache during code review.

Or maybe you've done something simpler but equally frustrating:

php
1<?php
2 
3// Creating a URL-friendly slug
4$slug = strtolower(
5 str_replace(' ', '-',
6 preg_replace('/[^a-z0-9\s]/i', '',
7 trim($title)
8 )
9 )
10);

The traditional "solution" for readability has been to use temporary variables:

php
1<?php
2 
3$trimmed = trim($title);
4$cleaned = preg_replace('/[^a-z0-9\s]/i', '', $trimmed);
5$hyphenated = str_replace(' ', '-', $cleaned);
6$slug = strtolower($hyphenated);

Better for readability? Sure. But now we've got four intermediate variables cluttering up our namespace, and honestly, coming up with meaningful names for each step gets old fast.

Enter the Pipe Operator

PHP 8.5 introduces the pipe operator (|>), and it's about to change how you think about data transformation. Here's that slug generation with pipes:

php
1<?php
2 
3$title = 'Hello World';
4 
5$slug = $title
6 |> trim(...)
7 |> (fn($title) => preg_replace('/[^a-z0-9\s]/i', '', $title))
8 |> (fn($title) => str_replace(' ', '-', $title))
9 |> strtolower(...);

Look at that flow! You can read it top to bottom, left to right, just like you'd explain the process to someone: "First we trim the title, then we remove special characters, then we replace spaces with hyphens, then we lowercase everything."

The ... syntax you're seeing is PHP's first-class callable syntax (introduced in 8.1), which works perfectly with pipes. The value from the left side gets passed as the first argument to the function on the right.

Real-World Laravel Examples

Let's get practical. Here at Jump24, we're all about Laravel, so let me show you some real examples from our projects.

Processing CSV Data for Import

We process a lot of CSV imports for our clients. Here's how pipes have cleaned up that code:

php
1<?php
2 
3public function importProducts(array $csvRows): Collection
4{
5 return $csvRows
6 |> (fn($rows) => array_map($this->normalizeRow(...), $rows))
7 |> (fn($rows) => array_filter($rows, $this->isValidProduct(...)))
8 |> (fn($rows) => array_map(Product::fromArray(...), $rows))
9 |> collect(...)
10 |> (fn($collection) => $collection->unique('sku'))
11 |> (fn($collection) => $collection->values());
12}
13 
14private function normalizeRow(array $row): array
15{
16 return [
17 'name' => trim($row['name'] ?? ''),
18 'price' => (float) ($row['price'] ?? 0),
19 'sku' => strtoupper(trim($row['sku'] ?? '')),
20 ];
21}
22 
23private function isValidProduct(array $row): bool
24{
25 return $row['price'] > 0 && !empty($row['sku']);
26}

Working with Match Expressions

Remember our post about PHP match expressions and enums? The pipe operator plays beautifully with match:

php
1<?php
2 
3public function formatContent(string $content, ContentFormat $format): string
4{
5 return match($format) {
6 ContentFormat::Markdown => $content
7 |> $this->parseMarkdown(...)
8 |> $this->sanitizeHtml(...)
9 |> $this->addTableOfContents(...),
10 
11 ContentFormat::PlainText => $content
12 |> strip_tags(...)
13 |> nl2br(...)
14 |> $this->linkifyUrls(...),
15 
16 ContentFormat::Html => $content
17 |> $this->sanitizeHtml(...)
18 |> $this->processShortcodes(...)
19 |> $this->lazyLoadImages(...),
20 };
21}

The Testing Benefits

One unexpected benefit we've discovered? Testing becomes easier. When you structure your code with pipes, you naturally end up with smaller, more focused functions:

php
1<?php
2 
3// Before: One big method that's hard to test
4public function processOrder(Order $order): Invoice
5{
6 // 50 lines of nested transformations and business logic
7}
8 
9// After: Smaller, testable pieces
10public function processOrder(Order $order): Invoice
11{
12 return $order
13 |> $this->validateOrder(...)
14 |> $this->calculateTotals(...)
15 |> $this->applyDiscounts(...)
16 |> $this->addTaxes(...)
17 |> $this->generateInvoice(...);
18}

Now we can unit test each step independently.

Type Safety Considerations

The pipe operator respects PHP's type system. When you have strict_types enabled, it enforces type checking at each step:

php
1<?php
2 
3declare(strict_types=1);
4 
5// This will throw a TypeError
6$result = 42
7 |> strlen(...); // TypeError: strlen() expects string, int given
8 
9// This works fine - type coercion in non-strict mode
10$result = 42
11 |> strval(...) // Convert to string first
12 |> strlen(...); // Now it works

This is actually great - it forces us to be explicit about type conversions, making our code more predictable.

The Reality Check

Now, let's be honest about the limitations, because you know we don't sugarcoat things here at Jump24.

Limited to First Parameter

The pipe operator only works cleanly with single-parameter functions or when the piped value should be the first parameter:

php
1<?php
2 
3// This is awkward
4$result = $data
5 |> (fn($x) => array_combine($keys, $x)) // Need value as second param
6 |> (fn($x) => array_merge($defaults, $x)); // Need value as second param

IDE Support Is Getting There

PHPStorm is catching up, but code completion and type inference in pipe chains isn't perfect yet. Though to be fair, JetBrains is usually pretty quick with updates.

Our Migration Strategy

We're not going crazy refactoring everything to use pipes. Here's our approach for when 8.5 is released:

  1. New code: Use pipes where they improve readability

  2. Refactoring: When touching code with deeply nested functions, consider pipes

  3. Hot paths: Leave them alone unless there's a clear benefit

  4. Code reviews: If someone can't understand it, we discuss whether pipes would help

  5. Team education: Everyone needs to understand them, not everyone needs to use them

How to Try It Now

PHP 8.5 releases on November 20, 2025, but you can try it today:

shell
1# Using Docker
2docker run -it php:8.5-rc-cli php -a
3 
4# On macOS with Homebrew
5brew tap shivammathur/php
6brew install shivammathur/php/php@8.5-dev
7 
8# In your composer.json for testing
9"require": {
10 "php": "^8.5"
11}

We're running the release candidate in our development environment (not production - we're not that brave), and it's been stable for our use cases.

The Bottom Line

After three weeks of using PHP 8.5's pipe operator in our development environment, I can confidently say it's improved our code quality. Not revolutionised, not transformed, but genuinely improved. Our code is more readable, our junior developers are picking things up faster, and we're naturally writing more testable functions.

Is it going to change the way you write PHP overnight? Probably not. But for those times when you're staring at five levels of nested function calls wondering if there's a better way - well, now there is.

The pipe operator lands with PHP 8.5 on November 20, 2025. If you're like us and can't wait, grab the release candidate and start experimenting. Your future self (and your code reviewers) will thank you.

Your Turn

What's the gnarliest nested function call in your codebase right now? Share it with us - we'd love to see how the pipe operator could clean it up. Drop us a line or share your examples in the comments.

And if you're dealing with complex data transformations in your Laravel applications and want to explore how modern PHP features like this can clean up your codebase, get in touch. We've been refactoring Laravel applications for over 11 years, and we love showing teams how the latest PHP features can solve real-world problems.

Want to stay updated on PHP 8.5 features? Follow us for more deep dives into what's coming and how to use it effectively in your Laravel projects.

Laravel Partner

Since 2014, we’ve built, managed and upgraded Laravel applications of all types and scales with clients from across the globe. We are one of Europe’s leading Laravel development agencies and proud to support the framework as an official Laravel Partner.