What's New in PHP 8.5?
PHP 8.5, released in November 2025, continues the trend toward new capabilities, simpler syntax and better typing.
Let's look at its main changes.
Pipe operator
The pipe operator (|>) lets you chain function calls without intermediate variables. So instead of writing this:
$input = ' Pipe operator in PHP 8.5 ';
$slug = strtolower(
str_replace('.', '',
str_replace(' ', '-',
trim($input)
)
)
);
or this:
$input = ' Pipe operator in PHP 8.5 ';
$temp = trim($input);
$temp = str_replace(' ', '-', $temp);
$temp = str_replace('.', '', $temp);
$slug = strtolower($temp);
you can now write this:
$input = ' Pipe operator in PHP 8.5 ';
$slug = $input
|> trim(...)
|> (fn($str) => str_replace(' ', '-', $str))
|> (fn($str) => str_replace('.', '', $str))
|> strtolower(...);
The operator passes the result of one function straight into the next one, which makes it convenient for tasks like this.
array_first() and array_last()
The array_first() and array_last() functions return the first or the last value of an array, respectively. If the array is empty, null is returned. So instead of using the ternary operator:
$firstEl = $array === []
? null
: $array[array_key_first($array)];
$lastEl = $array === []
? null
: $array[array_key_last($array)];
you can use the new functions:
$firstEl = array_first($array);
$lastEl = array_last($array);
These functions make working with arrays much easier.
grapheme_levenshtein()
The existing levenshtein() and mb_levenshtein() functions work with bytes or Unicode code points, which can give wrong results for complex Unicode characters such as emoji or letters with diacritics. The new grapheme_levenshtein() function calculates the Levenshtein distance based on graphemes — units of text that the user sees as a single character, even if they consist of several Unicode code points, for example:
var_dump(
grapheme_levenshtein(
"\u{00e9}", // é
"\u{0065}\u{0301}" // e + combining acute accent, also looks like é
)
); // Result: 0
The function treats the characters as equal even if their underlying encoding differs.
get_error_handler() and get_exception_handler()
In PHP, you can set your own error and exception handlers with set_error_handler() and set_exception_handler(). However, there used to be no simple way to check which handler is currently in use. The get_error_handler() and get_exception_handler() functions return the current error and exception handlers directly:
// Error handler
$handler = function (int $errno, string $errstr, ?string $errfile, ?int $errline) {
echo "Error: " . $errstr . "\n";
};
var_dump(get_error_handler()); // NULL until a handler is set
set_error_handler($handler);
var_dump(get_error_handler() === $handler); // bool(true), our handler is set
// Exception handler
$handler = function (Throwable $ex) {
echo "Exception: " . $ex::class . ": " . $ex->getMessage() . "\n";
};
var_dump(get_exception_handler()); // NULL until a handler is set
set_exception_handler($handler);
var_dump(get_exception_handler() === $handler); // bool(true), our handler is set
This is especially useful for libraries and frameworks that need to temporarily replace global error and exception handlers without losing the original ones.
Fatal error backtraces
Fatal errors (such as exceeding the execution time or memory limit) now include a backtrace. This is controlled by the new fatal_error_backtraces INI directive and makes debugging difficult cases much easier. The stack trace shows the sequence of function calls that led to the error, just like the output for exceptions.
URI extension
The URI extension provides an API for safely parsing and modifying URIs and URLs according to the RFC 3986 and WHATWG URL standards. For example:
use Uri\Rfc3986\Uri;
$uri = new Uri('https://avsoft-web.dev/new-in-php85');
echo $uri->getScheme(); // https
echo $uri->getHost(); // avsoft-web.dev
Starting with PHP 8.5, this extension is part of the PHP standard library.
Persistent cURL share handles
Unlike the older curl_share_init() function, handles created with curl_share_init_persistent() persist across PHP requests. If you call the function again with the same parameters, you get the same handle:
$share = curl_share_init_persistent([
CURL_LOCK_DATA_DNS, // Cache DNS records
CURL_LOCK_DATA_CONNECT, // Cache TCP/SSL connections
]);
$ch1 = curl_init("https://example.org");
curl_setopt($ch1, CURLOPT_SHARE, $share);
$response1 = curl_exec($ch1);
// On the next request to the same domain,
// DNS records and TCP/SSL connections are reused from the cache
$ch2 = curl_init("https://example.org/foo");
curl_setopt($ch2, CURLOPT_SHARE, $share);
$response2 = curl_exec($ch2);
PHP now manages the resource between requests itself, and you don't have to worry about storing or coordinating handles.
Clone as a function
By passing an associative array as the second argument to clone(), you can update properties while cloning objects. This lets you create modified copies without changing the original object:
class App {
public function __construct(public readonly string $env) {}
public function withEnv(string $env): static
{
return clone($this, ['env' => $env]);
}
}
$prod = new App('production');
$dev = $prod->withEnv('development');
This approach is very convenient for immutable objects and readonly properties.
Closures in constant expressions
PHP 8.5 allows static closures (anonymous functions) in constant expressions. A constant expression is a value that must be fully determined at compile time. Such expressions include attribute parameters, constants, and default values of properties and function parameters. An example with a custom attribute:
#[Validator(
static function(string $value): bool {
return preg_match('/^[a-z]+$/', $value);
}
)]
class User {
public string $username;
}
An example with a class constant:
class Formatter {
public const TO_UPPER = static function(string $str): string {
return strtoupper($str);
};
}
echo (Formatter::TO_UPPER)('php'); // PHP
And an example with a default value:
function upper_map(
array $data,
Closure $fn = static function ($x) { return strtoupper($x); }
) {
return array_map($fn, $data);
}
var_dump(upper_map(['a', 'b', 'c'])); // ['A', 'B', 'C']
There are some limitations, though. To be used in constant expressions, closures must be static, which means they can't use $this or capture variables from the surrounding scope. So you can't use use($foo) or arrow functions.
The #[\NoDiscard] attribute
The #[\NoDiscard] attribute lets developers indicate that a function's return value must not be ignored. If you add the attribute to a function, PHP checks whether the return value is used and emits a warning if it isn't:
#[\NoDiscard]
function setStatus(int $status): int
{
// Status change logic
return $status;
}
setStatus(5); // Warning
$result = setStatus(5); // No warning
This makes APIs safer when the return value matters but is easy to ignore.
Attributes on constants
You can now add attributes to constants declared with const:
#[Deprecated(reason: 'Use API_V2_URL instead. This will be removed in v2.0.')]
const API_V1_URL = 'https://api.example.com/v1';
This lets you attach metadata to them. Keep in mind that attributes can't be applied to constants created with define().
Deprecations and backward compatibility changes
The alternative scalar type casts (boolean), (integer), (double) and (binary) are deprecated. Use (bool), (int), (float) and (string) instead, respectively.
$value = (boolean) $foo; // Deprecated syntax
$value = (bool) $foo; // Recommended
Ending case statements with a semicolon instead of a colon is deprecated.
switch ($status) {
case 1; // Deprecated syntax
echo 'success';
break;
}
switch ($status) {
case 1: // Recommended
echo 'success';
break;
}
Using null as an array offset or as the key in array_key_exists() is deprecated. Use an empty string instead.
$array[null] = 'value'; // Deprecated behavior
array_key_exists(null, $array);
$array[''] = 'value'; // Recommended
array_key_exists('', $array);
Casting NAN to other types now emits a warning.
$value = (int) NAN; // Warning
Destructuring non-array values (other than null) with [] or list() now emits a warning as well.
[$a, $b] = 'test'; // Warning
A warning is emitted when casting floats (or float-like strings) to int if they can't be represented as an integer.
$value = (int) 1e20; // Warning
The __sleep() and __wakeup() magic methods have been soft-deprecated. The modern way to control serialization is __serialize() and __unserialize(), which give more control and predictability.
🐘 Is it worth upgrading?
PHP 8.5 is not a revolutionary release, but it makes code safer and easier to maintain by steadily fixing historical flaws of the language.
Upgrading is especially worthwhile for projects where predictable behavior, strict typing and long-term maintainability matter — for example, when developing libraries and frameworks.
To avoid unexpected problems, make sure your dependencies and libraries are compatible before upgrading.
Published