What is new in PHP 8.5 vs PHP 8.0
Pipe operator
Nested calls read inside-out. The |> operator chains callables left to right, passing each result into the next function without helper variables:
$slug = $title |> trim(...) |> (fn($str) => str_replace(" ", "-", $str)) |> strtolower(...);
Either a first-class callable (trim(...)) or an arrow function fits into a stage. Anything longer than one expression still belongs into a named function - pipes reward short stages.
URI extension
parse_url() returns a plain array and every project validates it differently. PHP 8.5 adds an always-available URI extension following RFC 3986 and the WHATWG URL standard:
use Uri\Rfc3986\Uri; $uri = new Uri("https://php.net/releases/8.5/en.php"); var_dump($uri->getHost()); // string(7) "php.net"
URIs can be parsed, normalized, and modified through objects instead of string surgery, which closes a whole class of validation bugs.
Clone with properties
Readonly classes cannot be modified after construction, so every "wither" rebuilt the object by hand. Now clone() accepts an associative array of property overrides:
readonly class Color { public function __construct( public int $red, public int $green, public int $blue, public int $alpha = 255, ) {} public function withAlpha(int $alpha): self { return clone($this, ["alpha" => $alpha]); } }
#[NoDiscard] attribute
Forgetting a return value is a silent bug - the call succeeds and the result evaporates. Mark the function and PHP warns when the value is dropped:
#[NoDiscard] function getPhpVersion(): string { return "PHP 8.5"; } getPhpVersion(); // Warning: return value should be used
A deliberate (void) cast marks the value as intentionally unused and silences the warning.
Callables in constant expressions
Static closures and first-class callables now work where only constants were allowed: attribute arguments, property and parameter defaults:
final class PostsController { #[AccessControl(static function (Request $request): bool { return $request->user !== null; })] public function update(Request $request): Response { // ... } }
Persistent cURL share handles
Share handles created with curl_share_init() died at the end of each request. curl_share_init_persistent() reuses a handle with the same share options across requests, so DNS and connection caches survive:
$sh = curl_share_init_persistent([ CURL_LOCK_DATA_DNS, CURL_LOCK_DATA_CONNECT, ]); $ch = curl_init("https://php.net/"); curl_setopt($ch, CURLOPT_SHARE, $sh); curl_exec($ch);
array_first() and array_last()
Reading an edge of an array took a ternary with array_key_first(). The new helpers return the value directly and null for an empty array, composing neatly with ??:
$lastEvent = array_last($events); $first = array_first($events) ?? new Event();
Smaller improvements
- Attributes can target constants; #[Override] applies to properties; #[Deprecated] applies to traits and constants.
- Static properties support asymmetric visibility; properties can be marked final through constructor promotion.
- Closure::getCurrent() simplifies recursion in anonymous functions; new get_error_handler() and get_exception_handler() functions.
- setcookie() supports the "partitioned" key; grapheme_levenshtein() for Unicode-aware distance; fatal errors now include a backtrace.
Deprecations and breaks
- Backtick operator (alias of shell_exec()) is deprecated.
- Non-canonical casts (boolean), (integer), (double), (binary) are deprecated - use (bool), (int), (float), (string).
- The disable_classes INI setting is removed; terminating case with a semicolon is deprecated.
- __sleep() and __wakeup() are soft-deprecated in favor of __serialize() and __unserialize().
- New warnings: casting NAN, unrepresentable float-to-int casts, destructuring non-arrays, and null as array offset.
Released 20 November 2025. Read the other end of the timeline: what is new in PHP 8.0 vs PHP 7.
Article author: Andrei Olegovich