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:
$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-operatorBefore 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.
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=3The 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:
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:
#[\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 warningThe 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:
$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.finalon promoted properties, and asymmetric visibility for static properties.get_error_handler()andget_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
casewith a semicolon instead of a colon is deprecated. - Using
nullas an array offset, or passing it toarray_key_exists(), is deprecated. __sleep()and__wakeup()are soft-deprecated in favour of__serialize()and__unserialize().- The
disable_classesINI 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.