PHP 8.5 is mostly additive at the language level, so most PHP 8.4 code keeps working. The new features reward you if you adopt them, while the deprecations and new warnings are what you need to find and fix before you upgrade. This page covers language behaviour only; library, extension and INI changes are covered elsewhere.
The headline feature is the pipe operator |>, which passes the value on its left to a one-argument callable on its right. It turns deeply nested calls into a readable top-to-bottom chain. Alongside it, clone can now be called like a function that accepts property overrides, which makes immutable "with-er" methods on readonly classes far less clumsy.
There are also several smaller capabilities. Closures and first-class callables are allowed in constant expressions, #[\NoDiscard] and the (void) cast let you mark return values that must not be ignored, #[\Override] works on properties, static properties can use asymmetric visibility, and constructor promotion accepts final. The manual also lists attributes on compile-time constants, casts in constant expressions and backtraces for fatal errors; these are not demonstrated below.
The upgrade work is mostly in deprecations and stricter diagnostics. The non-canonical cast names such as (integer) and (double), terminating a case with a semicolon, the backtick operator, and using null as an array offset all now emit deprecations. Casting a float that cannot be represented as an integer, casting NAN, and destructuring a non-array value now emit warnings. If your project turns warnings into exceptions, these can surface as new failures in code that previously ran quietly.
The incompatible-changes page also records more subtle engine adjustments: traits are bound before the parent class, errors during compilation and class linking are delayed, and loose comparison of uncomparable objects with booleans now follows a boolean cast. Review any code that depends on those details.
A safe upgrade path is to run your test suite on PHP 8.4 with deprecations enabled, fix everything it reports, and then move to 8.5 and look for the new warnings. Adopt the new syntax only after your minimum supported version is 8.5, because none of it parses on older releases.
NEW
Pipe operator for chaining callables
The |> operator feeds the value on its left into a callable on its right, replacing nested calls with a linear chain.
PHP 8.5 adds the pipe operator |>. The expression on the left is passed as the single argument to the callable on the right, and the result flows into the next stage. First-class callable syntax such as trim(...) fits naturally, so a transformation that used to be written inside-out can now read in execution order.
Each stage must be a callable that accepts one argument, so functions needing extra arguments have to be wrapped in a closure or arrow function. Wrap an arrow function in parentheses when you place it in a chain, otherwise the parser cannot tell where the function body ends. Because every stage hands exactly one value to the next, the pipe works best for small, pure transformations.
The pipe does not change what the functions do; it only changes how you write the composition. Be aware that none of this syntax parses on PHP 8.4 or earlier, so adopt it only once 8.5 is your minimum version. Keep chains short enough to read, and extract named functions when a stage gets complicated.
$slug = str_replace(' ', '-', strtolower(trim(' PHP 8.5 Release ')));
echo $slug, "\n";Valid in PHP 8.4 and 8.5, but it reads from the inside out. Shown for comparison; not executed on this site.
$slug = " PHP 8.5 Release "
|> trim(...)
|> strtolower(...)
|> (fn(string $s): string => str_replace(' ', '-', $s));
echo $slug, "\n";php-8.5-release
NEW
clone as a function with property overrides
clone can be called like a function and accept an array of property values to apply to the copy, including readonly properties.
In PHP 8.5, clone behaves like a function and accepts a second argument, $withProperties, an array of property names and new values applied to the copy. The manual notes that this also lets you reassign readonly properties during cloning, which was previously awkward because readonly properties could not be modified after initialization.
This makes immutable "with-er" methods much shorter. Instead of calling the constructor again and repeating every unchanged argument, you clone the object and name only the properties that differ. Adding a new property to the class no longer requires updating every with-er method.
The pitfall is that you now have two ways to build modified copies, so keep your team's convention consistent. The override array uses property names as keys, so a typo in a key is a runtime issue rather than a compile-time one; cover with-er methods with tests. The function-call form of clone is PHP 8.5 only.
final class Point {
public function __construct(public readonly int $x, public readonly int $y) {}
public function withX(int $x): static {
return new static($x, $this->y);
}
}
echo (new Point(1, 2))->withX(10)->x, "\n";Works on PHP 8.4 and 8.5, but every unchanged property must be passed again. Shown for comparison; not executed on this site.
final class Point {
public function __construct(public readonly int $x, public readonly int $y) {}
public function withX(int $x): static {
return clone($this, ['x' => $x]);
}
}
$a = new Point(1, 2);
$b = $a->withX(10);
echo "{$a->x},{$a->y} {$b->x},{$b->y}\n";1,2 10,2
NEW
#[\NoDiscard] and the (void) cast
A function can declare that its return value matters, and callers can deliberately ignore it with a (void) cast.
PHP 8.5 adds the #[\NoDiscard] attribute for functions whose return value is important. If a caller invokes such a function as a bare statement and throws the result away, PHP emits a warning. Using the value, for example by assigning it, silences the warning.
The companion (void) cast documents that ignoring a value is intentional. By itself it has no effect on how the program runs, but it suppresses the #[\NoDiscard] warning and may also quiet diagnostics from IDEs and static analysis tools.
This is most useful on functions that report success or failure through a return value rather than an exception. The migration risk is limited to code you annotate yourself, or to attributed functions you call, since a bare call to such a function now produces a warning. Do not sprinkle (void) everywhere; it should mark the few places where discarding really is deliberate. On PHP 8.4 the attribute is just an unused attribute, but the (void) cast is a syntax error.
#[\NoDiscard]
function save(array $rows): bool {
return count($rows) > 0;
}
$ok = save(['a']); // value is used
(void) save([]); // intentionally ignored
echo $ok ? "saved" : "empty", "\n";saved
NEW
Closures in constant expressions
Closures and first-class callables are now allowed in constants, class constants, attribute arguments and default values.
PHP 8.5 allows closures and first-class callable syntax inside constant expressions. According to the manual, that covers attribute parameters, default values of properties and parameters, and both global and class constants.
The practical gain is that a parameter can have a real callable default. Before, you had to default to null and substitute the fallback inside the function body, which hid the default from the signature and from reflection. Now the default is declared right where the parameter is.
A closure used in a constant expression has to be self-contained: write it as static, since there is no object for it to bind to. Arrow functions (fn) are rejected in constant expressions with a fatal error; use a static function closure instead. Also remember that a constant holding a closure must be copied into a variable before you call it, because writing the constant name followed by parentheses is a function call. As with all new syntax, this does not parse on PHP 8.4.
final class Rule {
public Closure $check;
public function __construct(?Closure $check = null) {
$this->check = $check ?? static fn(int $n): bool => $n > 0;
}
}
$r = new Rule();
var_dump(($r->check)(5));Works on PHP 8.4 and 8.5; the real default is hidden inside the constructor body. Shown for comparison; not executed on this site.
final class Rule {
public function __construct(
public Closure $check = static function (int $n): bool { return $n > 0; }
) {}
}
const DOUBLE = static function (int $n): int { return $n * 2; };
$r = new Rule();
$double = DOUBLE;
echo json_encode([($r->check)(5), $double(4)]), "\n";[true,8]
NEW
Asymmetric visibility for static properties
Static properties can now have a wider read visibility than write visibility, such as public private(set).
Asymmetric visibility, which PHP 8.4 offered for instance properties, now extends to static properties in PHP 8.5. A declaration such as public private(set) static int $count lets any code read the value while only the declaring class can write it.
This suits shared counters, registries and other class-level state where outside code needs read access but must not modify the value directly. It replaces the older pattern of a private static property plus a public static getter, which was more code and less obvious.
When outside code attempts a write that the write visibility forbids, PHP throws an Error, so tests should cover any code that previously assigned such a property from outside. The write scope you choose applies to everything outside it, including subclasses if you pick private(set). Pick protected(set) when subclasses need to update the value. The syntax is not available on PHP 8.4 for static properties.
final class Registry {
private static int $count = 0;
public static function add(): void { self::$count++; }
public static function count(): int { return self::$count; }
}
Registry::add();
echo Registry::count(), "\n";Works on PHP 8.4 and 8.5, but needs a getter method to expose the value. Shown for comparison; not executed on this site.
final class Registry {
public private(set) static int $count = 0;
public static function add(): void { self::$count++; }
}
Registry::add();
Registry::add();
echo Registry::$count, "\n";
try {
Registry::$count = 99;
} catch (Error $e) {
echo $e::class, "\n";
}2 Error
NEW
#[\Override] on properties
The Override attribute can now be applied to properties, so a child class can declare that it intentionally redefines an inherited property.
The #[\Override] attribute previously applied to methods, where it makes PHP complain if nothing in a parent class or interface is actually being overridden. In PHP 8.5 it can also be attached to properties.
This catches a common refactoring accident: a parent property is renamed or removed, and a child class's redeclaration silently becomes a brand-new, unrelated property. With the attribute in place, the engine reports an error instead, so the mismatch is found at once rather than through odd behaviour later.
The attribute does not change the value or visibility of the property; it only states intent and enables the check. Add it to child-class property redeclarations such as changed defaults. If a property is marked but the parent does not have it, expect a fatal error, so apply it deliberately. On PHP 8.4, using it on a property is not valid.
class Base {
public string $label = 'base';
}
class Child extends Base {
#[\Override]
public string $label = 'child';
}
echo (new Child)->label, "\n";child
NEW
Constructor promotion for final properties
A promoted constructor parameter can now carry the final modifier, so the property cannot be redeclared in subclasses.
Constructor property promotion lets you declare and assign a property in the constructor signature. Until PHP 8.5 you could not combine it with final, so a property that subclasses must not redeclare needed a separate declaration plus a manual assignment.
Now the modifier can sit directly on the promoted parameter. The class itself does not need to be final; only the property is protected from being overridden in child classes. This keeps the constructor signature as the single place that describes the property.
The migration impact is small: existing code keeps working, and you can tidy classes that used the longer pattern. Remember that a final property still has to be declared with a visibility that subclasses can see, and that this syntax is a parse error on PHP 8.4, so do not use it in code that must still run there.
class User {
final public string $name;
public function __construct(string $name) {
$this->name = $name;
}
}
echo (new User('Ada'))->name, "\n";Works on PHP 8.4 and 8.5; promotion could not be combined with final. Shown for comparison; not executed on this site.
class User {
public function __construct(final public string $name) {}
}
echo (new User('Ada'))->name, "\n";Ada
DEPRECATED
Non-canonical cast names are deprecated
The cast spellings (boolean), (integer), (double) and (binary) now emit deprecations; use (bool), (int), (float) and (string).
PHP has long accepted alternative spellings for its scalar casts. In PHP 8.5, (boolean), (integer), (double) and (binary) are deprecated. The replacements are the canonical forms: (bool), (int), (float) and (string).
The code still runs on 8.5, but each use emits a deprecation, which will be noisy in logs and fatal if your error handler converts deprecations into exceptions. The fix is mechanical, so it can be done with a search and replace or an automated refactoring tool such as a code-style fixer, then checked by the test suite.
Pay attention to (binary), which is the least obvious: it maps to (string). The canonical casts work identically on PHP 8.4 and 8.5, so you can make this change before you upgrade and it is safe on both versions. Also search vendored code and templates, since the deprecation fires wherever the cast is compiled.
$n = (integer) "42";
$f = (double) "1.5";
$b = (boolean) "yes";Accepted silently by PHP 8.4; deprecated in PHP 8.5. Shown for comparison; not executed on this site.
$n = (int) "42";
$f = (float) "1.5";
$b = (bool) "yes";
echo json_encode([$n, $f, $b]), "\n";[42,1.5,true]
DEPRECATED
Semicolon-terminated case labels are deprecated
Ending a case statement with a semicolon instead of a colon is deprecated; always use a colon.
PHP has historically tolerated a semicolon after a case label in a switch, treating it like the usual colon. PHP 8.5 deprecates that form. The code still behaves the same, but it now emits a deprecation message when the file is compiled.
This is usually found in very old code or in hand-edited legacy files, and it is easy to miss because it never caused a visible problem. A project-wide search for case lines ending in a semicolon is a quick way to find candidates, and a syntax linter run under PHP 8.5 will report them as well.
Changing the semicolon to a colon is safe on every supported PHP version, so you can fix it ahead of the upgrade. Since the deprecation is raised at compile time, it can appear for files that are never executed in a given request, for instance when they are loaded through an autoloader. Check the default label too if it was written the same way.
switch ($n) {
case 1;
echo "one";
break;
}Accepted silently by PHP 8.4; deprecated in PHP 8.5. Shown for comparison; not executed on this site.
function label(int $n): string {
switch ($n) {
case 1:
return "one";
default:
return "many";
}
}
echo label(1), " ", label(5), "\n";one many
DEPRECATED
null as an array offset is deprecated
Using null as an array key, or passing it to array_key_exists(), is deprecated; use an explicit empty string instead.
PHP silently converted a null array offset to the empty string key. PHP 8.5 deprecates that implicit conversion, both for ordinary offsets and when null is passed as the key to array_key_exists(). The manual's recommended replacement is an explicit empty string.
The risk is in code where the key comes from a variable that may be null, such as an optional lookup value or an unset form field. These paths may only run in rare cases, so the deprecation can appear late in production logs. Decide whether a missing key should really map to the empty-string entry. If it should not, handle the null case separately instead of papering over it.
When you do want the old behaviour, make it visible with the null coalescing operator, which works on every PHP version. Static analysis that tracks nullable types is the best way to find these spots before upgrading, because the dynamic behaviour only shows up when the null value actually reaches the array access.
$map = [];
$key = null;
$map[$key] = 'empty';
var_dump(array_key_exists($key, $map));Accepted silently by PHP 8.4; deprecated in PHP 8.5. Shown for comparison; not executed on this site.
$map = [];
$key = null;
$map[$key ?? ''] = 'empty';
echo json_encode($map), "\n";{"":"empty"}
CHANGED BEHAVIOR
Warnings for unrepresentable float-to-int casts
Casting a float, float-like string or NAN that cannot be an integer now emits a warning instead of failing silently.
PHP 8.5 emits a warning when a float, or a string that looks like a float, is cast to int and cannot be represented as one. This covers explicit (int) casts and implicit conversions. Casting NAN to other types also warns now.
Previously these conversions gave a meaningless result without any signal, so a bad value could travel through a program unnoticed. The warning exposes those cases, but it can also surface in code that appeared to work, particularly arithmetic that overflows into very large floats or parsing of untrusted numeric input. Be especially careful if you convert warnings to exceptions.
The robust fix is to check the range before casting and decide explicitly what a non-representable value should mean, such as returning null or rejecting the input. Ordinary fractional values such as 42.9 are still truncated by an explicit cast without any warning. The range check below also rejects NAN and infinity because comparisons with them are false.
$big = 1e20;
$n = (int) $big;
echo gettype($n), "\n";Runs silently on PHP 8.4 with a meaningless integer; PHP 8.5 emits a warning for the unrepresentable value. Shown for comparison; not executed on this site.
function toInt(float $v): ?int {
return $v >= -9.2233720368547758E+18 && $v < 9.2233720368547758E+18
? (int) $v
: null;
}
echo json_encode([toInt(42.9), toInt(1e20), toInt(NAN)]), "\n";[42,null,null]
CHANGED BEHAVIOR
Destructuring non-array values now warns
Using [] or list() to destructure anything other than an array or null now emits a warning.
Destructuring with [] or list() has always been forgiving: if the right-hand side was a scalar, the variables simply became null. PHP 8.5 emits a warning when you destructure a non-array value, with null still allowed without a warning.
This matters for code that destructures the result of a function with a loose return type, such as a parser returning false on failure or a lookup returning an integer. Such calls used to produce silent nulls and now log warnings, and with a strict error handler they become exceptions.
The fix is to validate the value before destructuring. Check with is_array() and supply an explicit fallback, or change the function to return a consistent array or null. Do this rather than suppressing the warning, because the warning points to a path that is probably unhandled. Because the behaviour change is only a diagnostic, the guarded form below runs identically on PHP 8.4 and 8.5.
[$a, $b] = 5;
var_dump($a, $b);Runs silently on PHP 8.4, leaving both variables null; PHP 8.5 emits a warning. Shown for comparison; not executed on this site.
function pair(mixed $value): array {
[$a, $b] = is_array($value) ? $value : [null, null];
return [$a, $b];
}
echo json_encode([pair([1, 2]), pair(null), pair(5)]), "\n";[[1,2],[null,null],[null,null]]