PHP 8.5, released on 20 November 2025, is a quality-of-life release. Nothing in it forces you to rewrite code, but several additions remove boilerplate you have been writing for years: a pipe operator for chaining functions, a standards-compliant URI parser in the core, a clean way to copy an object with changes, and array_first and array_last at long last.
The pipe operator#
|> passes the value on its left into the callable on its right. Instead of nesting calls inside out, you read them top to bottom:
// Before: read from the inside out
$slug = strtolower(trim(preg_replace('/[^A-Za-z0-9]+/', '-', $title), '-'));
// PHP 8.5: read in order
$slug = $title
|> fn($s) => preg_replace('/[^A-Za-z0-9]+/', '-', $s)
|> fn($s) => trim($s, '-')
|> strtolower(...);
Each step is a callable taking one argument. First-class callable syntax, strtolower(...), fits naturally; for functions that need extra arguments, an arrow function wraps them. Only single-value pipes are supported — there is no automatic spreading of arrays into parameters.
Clone with#
Readonly classes are immutable, which made the “copy with one property changed” pattern awkward: you could not modify a clone’s readonly properties. 8.5 adds an argument to clone:
final readonly class Money {
public function __construct(public int $amount, public string $currency) {}
public function withAmount(int $amount): static {
return clone($this, ['amount' => $amount]);
}
}
$fee = new Money(500, 'GBP');
$doubled = $fee->withAmount(1000);
The array keys are property names. Readonly properties may be set this way during the clone, and the original is untouched. This makes value objects in PHP pleasant to write for the first time.
The URI extension#
parse_url has always been a rough approximation: it does not follow a standard, mishandles some valid URLs, and cannot resolve relative references. 8.5 adds a URI extension, always available, with two parsers:
use Uri\Rfc3986\Uri;
use Uri\WhatWg\Url;
$uri = new Uri('https://example.com:8443/docs/page?x=1#top');
$uri->getHost(); // example.com
$uri->getPort(); // 8443
$uri->getQuery(); // x=1
// Resolve a relative reference against a base
$next = Uri::parse('../images/logo.png', 'https://example.com/docs/page');
echo $next->toString(); // https://example.com/images/logo.png
// The WHATWG parser matches what browsers do
$url = new Url('HTTPS://EXAMPLE.com/a/../b');
echo $url->toAsciiString(); // https://example.com/b
Use the RFC 3986 class for APIs and general processing; use the WHATWG class when you want the same normalisation a browser would apply. Both are immutable, with with methods returning modified copies.
NoDiscard#
Some functions return a value that is a mistake to ignore — a new immutable object, a result you must check. Mark them:
#[\NoDiscard]
function withTimeout(int $seconds): self { ... }
withTimeout(30); // warning: the return value is not used
(void) withTimeout(30); // explicit discard, no warning
It is a warning, not an error, and the new (void) cast documents the rare case where discarding is intended.
array_first and array_last#
$items = ['b' => 2, 'c' => 3, 'a' => 1];
array_first($items); // 2 - the first value, whatever its key
array_last($items); // 1
array_first([]); // null
These complement array_key_first and array_key_last from PHP 7.3. The old workarounds — reset(), which moves the internal pointer, or $items[array_key_first($items)] — can finally go.
Closure::getCurrent#
A recursive anonymous function used to need a use (&$fn) reference to itself. Now:
$factorial = function (int $n): int {
return $n <= 1 ? 1 : $n * Closure::getCurrent()($n - 1);
};
Smaller additions#
get_error_handler()andget_exception_handler()return the currently installed handlers, so libraries can wrap rather than replace them.#[\DelayedTargetValidation]lets an attribute be declared on a target that older PHP versions would reject at compile time, easing multi-version libraries.- The
PHP_BUILD_DATEconstant, andphp --ini=diffto print only settings that differ from the defaults. - Fatal errors now include a backtrace by default, which makes production logs far more useful.
Deprecations#
These emit deprecation notices now and will be removed in PHP 9:
- The non-canonical cast names
(integer),(boolean),(double)and(binary). Use(int),(bool),(float)and(string). - The
__sleepand__wakeupmagic methods, in favour of__serializeand__unserialize. - Returning a non-array from
__debugInfo, and a handful of internal-function argument quirks.
# Find the cast names before they find you
grep -rnE '\((integer|boolean|double|binary)\)' src/
Upgrading from 8.4#
For most applications 8.5 is a drop-in upgrade. The checklist:
- Confirm your framework’s minimum: Laravel, Symfony and WordPress all supported 8.5 within weeks of release, but check your exact version.
- Update extensions built from source, and any that pin a PHP version in Composer.
- Grep for the deprecated cast names.
- Run the tests, then run the application with
display_errorson in a staging environment and watch for notices.
Questions people ask#
Is PHP 8.5 a major release?
No. It is a minor release in the 8.x line, backwards compatible apart from the deprecations listed. PHP 9 is still some way off.
How long is 8.5 supported?
Two years of active support followed by two years of security fixes, the same as every 8.x release.
Does the pipe operator work with methods?
Anything callable works on the right-hand side: named functions, arrow functions, first-class callables including $obj->method(...), and static method callables.
Should I replace parse_url everywhere?
For new code, use the URI classes. Existing parse_url calls that work can stay; it is not deprecated.
Where to go next#
- PHP 7 vs PHP 8 — if you are further behind.
- How to check which PHP version is running — before and after upgrading.
- PHP basics — the language reference.