What's New in PHP 8.5
PHP 8.5 leans into composition and ergonomics. Here are its headline features with practical examples: the pipe operator, cloning with property updates, the NoDiscard attribute, array_first and array_last, the new URI extension, and backtraces on fatal errors.
PHP 8.5 was released on 20 November 2025, closing out a remarkable run of yearly releases. Where 8.4 was about how you design classes, 8.5 is about how you compose and use them: a pipe operator for chaining, a clean way to clone-with-changes, and a built-in URI parser, among others. Here is the tour, and the end of our series.
The pipe operator
The pipe operator |> takes the value on its left and feeds it as the argument to the callable on its right, so you can read a transformation as a top-to-bottom pipeline instead of an inside-out nest of function calls.
$slug = ' Hello, Fellow PHP Devs! '
|> trim(...)
|> strtolower(...)
|> (fn (string $s) => preg_replace('/[^a-z0-9]+/', '-', $s))
|> (fn (string $s) => trim($s, '-'));
echo $slug; // "hello-fellow-php-devs"
Each step gets the previous step's output. Compare that to trim(preg_replace(..., strtolower(trim($input)))), which you have to read from the inside out. The right-hand side is any callable, so the first-class callable syntax from 8.1 and short closures slot right in.
Cloning with property updates
PHP 8.3 made it possible to deep-clone readonly objects inside __clone(). PHP 8.5 goes further: clone() now takes an array of property values to overwrite on the copy, which makes the immutable "with-er" pattern a single expression.
final class Color
{
public function __construct(
public readonly int $r,
public readonly int $g,
public readonly int $b,
public readonly int $alpha = 255,
) {}
public function withAlpha(int $alpha): static
{
return clone($this, ['alpha' => $alpha]);
}
}
$blue = new Color(0, 0, 255);
$ghost = $blue->withAlpha(64); // a new Color, original untouched
The original object is never mutated; you get a fresh copy with just the named properties changed. This is exactly the immutable update that used to require a full constructor call repeating every unchanged field.
The NoDiscard attribute
Some return values are too important to ignore: the boolean that says whether a write succeeded, the new immutable object from a with-er. #[NoDiscard] makes PHP warn when a call's return value is thrown away.
final class Config
{
#[NoDiscard('the returned Config is the updated one; this is immutable')]
public function withDebug(bool $on): static
{
return clone($this, ['debug' => $on]);
}
}
$config->withDebug(true); // Warning: return value not used
$config = $config->withDebug(true); // correct
(void) $config->withDebug(true); // explicitly discard, no warning
It catches the very common mistake of calling an immutable method and forgetting to capture its result, which otherwise does nothing at all. If you really mean to discard the value, casting to (void) says so and silences the warning.
array_first and array_last
PHP 7.3 gave us array_key_first() and array_key_last(). PHP 8.5 completes the set with array_first() and array_last(), which return the first and last values directly.
$events = $log->recent();
$latest = array_last($events); // last value, or null if empty
$oldest = array_first($events); // first value, or null if empty
No more $arr[array_key_last($arr)] or end($arr) with its internal-pointer side effects. They return null on an empty array, so they pair naturally with the null coalescing operator and the array_find family from 8.4.
The URI extension
Parsing URLs in PHP has long meant parse_url(), which is loose about edge cases and not standards-compliant. PHP 8.5 ships a proper URI extension with classes that follow RFC 3986 and the WHATWG URL standard.
use Uri\Rfc3986\Uri;
$uri = new Uri('https://example.com:8080/blog/post?ref=newsletter#top');
$uri->getScheme(); // "https"
$uri->getHost(); // "example.com"
$uri->getPort(); // 8080
$uri->getPath(); // "/blog/post"
$uri->getQuery(); // "ref=newsletter"
You get real parsing, normalization, and validation instead of a best-effort associative array. For anything that handles user-supplied links, that correctness matters: it is the difference between rejecting a malformed URL and quietly mishandling it.
Backtraces on fatal errors
A long-standing debugging annoyance: fatal errors printed a message but no stack trace, so you knew what broke but not how you got there. PHP 8.5 includes a backtrace with fatal errors by default.
; enabled by default in PHP 8.5
fatal_error_backtraces = On
Now a fatal error shows the call chain that led to it, the same way an uncaught exception always has. It is a quiet change, but the first time a production fatal hands you a full trace instead of a single cryptic line, you will appreciate it.
That wraps our walk from PHP 8.0 to 8.5. Step back and the arc is clear: 8.0 modernized the syntax, 8.1 and 8.2 made immutability and types first-class, 8.3 and 8.4 reshaped how you design classes, and 8.5 makes composing them a pleasure. If you started this series on the 8.0 article, the contrast between then and now is its own little lesson in how far the language has come.