Skip to content

PHP 8.5: The Pipe Operator and Friends โ€‹

PHP 8.5 is scheduled for general availability on 20 November 2025. It's a nice follow-up to 8.4. Where 8.4 was about classes and properties, 8.5 is about how data flows through your code.

Here's what I think is worth your attention. I've only included features that are in the 8.5 release notes.


๐Ÿ”— The Pipe Operator |> โ€‹

This is the headline. The pipe operator RFC passed 33 to 7 in May. |> takes the value on its left and passes it as the only argument to the callable on its right:

php
$slug = '  PHP 8.5: The Pipe Operator  '
    |> trim(...)
    |> strtolower(...)
    |> (fn (string $s): string => preg_replace('/[^a-z0-9]+/', '-', $s))
    |> (fn (string $s): string => trim($s, '-'));

// php-8-5-the-pipe-operator

Before this, you either nested the calls inside out or created a pile of temporary variables. Now the code reads in the order it runs.

The rules are worth learning before you go pipe-crazy:

  • The right-hand side must be a callable that takes one argument. Functions that need more arguments have to be wrapped in a closure.
  • Arrow functions must be wrapped in parentheses, as above. Otherwise they swallow the rest of the chain.
  • By-reference parameters aren't allowed, so sort() and friends are out.
  • It compiles down to ordinary function calls, so there's essentially no runtime cost.

My take is that it shines for transformation pipelines, such as normalising input or shaping API payloads. I wouldn't use it to replace a simple method chain on an object.


๐ŸŒ A Real URI Extension โ€‹

parse_url() has never followed any standard. PHP 8.5 adds an always-available URI extension with two implementations. Uri\Rfc3986\Uri follows RFC 3986, and Uri\WhatWg\Url follows the WHATWG URL standard that browsers use.

php
use Uri\Rfc3986\Uri;

$uri = new Uri('https://api.example.com/v1/orders?page=2');

echo $uri->getHost();  // api.example.com
echo $uri->getQuery(); // page=2

$next = $uri->withQuery('page=3');
echo $next->toString(); // https://api.example.com/v1/orders?page=3

The objects are immutable. The constructor throws on invalid input, and the static parse() returns null instead, so you no longer get a half-parsed array back. If your code builds signed URLs, validates webhooks or checks redirects, this is worth moving to.


๐Ÿงฌ Clone With โ€‹

Updating a readonly object used to mean reflection tricks or rebuilding it through the constructor. Now clone() takes an array of properties to change:

php
final readonly class Money
{
    public function __construct(
        public int $amount,
        public string $currency,
    ) {}

    public function withAmount(int $amount): static
    {
        return clone($this, ['amount' => $amount]);
    }
}

The usual visibility rules still apply. Public readonly properties are protected(set) by default, so you can't use this to modify an object's internals from outside the class. That's the right call.


๐Ÿšซ #[\NoDiscard] โ€‹

Some return values should never be ignored, like a result object, a "did this succeed?" boolean or a new immutable instance. Mark the function and PHP warns when the result is dropped:

php
#[\NoDiscard]
function reserveStock(string $sku, int $quantity): bool
{
    return true;
}

reserveStock('SKU-1', 2);        // Warning: the return value should be used
(void) reserveStock('SKU-1', 2); // explicitly ignored, no warning

The new (void) cast is how you say you meant to ignore the result. I plan to add this to every with*() method on value objects, where dropping the result is almost always a bug.


๐Ÿงบ array_first() and array_last() โ€‹

We've had array_key_first() and array_key_last() since 7.3. Now we can get the values too, without the reset()/end() pointer games:

php
$latest = array_last($events) ?? throw new RuntimeException('No events');

Both return null for an empty array, so they work well with ??.


๐Ÿงฉ Smaller Things I'll Actually Use โ€‹

  • Fatal errors now include a backtrace, so a timeout or out-of-memory error finally tells you where it happened.
  • Static closures and first-class callables in constant expressions, including attribute arguments.
  • Persistent cURL share handles via curl_share_init_persistent(). These let DNS and connection data be reused across requests.
  • #[\Override] on properties, and #[\Deprecated] on traits and constants.
  • final on promoted properties, and asymmetric visibility for static properties.
  • get_error_handler() and get_exception_handler(), which framework code has wanted for years.

โš ๏ธ Deprecations โ€‹

These are the 8.5 deprecations you're most likely to hit:

  • The backtick operator (an alias for shell_exec()) is deprecated.
  • Non-canonical casts like (integer), (boolean), (double) and (binary) are deprecated. Use (int), (bool), (float) and (string).
  • Ending a case with a semicolon instead of a colon is deprecated.
  • Using null as an array offset, or passing it to array_key_exists(), is deprecated.
  • __sleep() and __wakeup() are soft-deprecated in favour of __serialize() and __unserialize().
  • The disable_classes INI setting has been removed.

The null offset one is the sneaky one. It tends to hide in old code that builds arrays from database rows with nullable keys.


๐ŸŽฏ Conclusion โ€‹

PHP 8.5 isn't a huge release, but it's a very practical one. The pipe operator gets the attention. Day to day, I expect the URI extension, clone-with and #[\NoDiscard] to prevent more real bugs.

If you upgraded to 8.4 this year, the jump to 8.5 should be gentle. Run your test suite on the release candidate now, fix the deprecations, and you'll be ready on release day.