PHP 8.2 is a moderate upgrade from PHP 8.1 at the language level. Most of what it adds is opt-in: you can adopt readonly classes, disjunctive normal form (DNF) types, the stand-alone true, null and false types, constants in traits, and enum property fetches in constant expressions whenever you like. Existing code keeps working.
The part that needs planning is the set of deprecations. Creating a dynamic property on an object whose class has not opted in now emits a deprecation. This is the change most likely to light up logs in older codebases, particularly in legacy models, data-transfer objects, and test doubles that assign properties they never declared. Two other patterns are deprecated as well: callables such as "self::method" or ["parent", "method"] that call_user_func() accepts but the $callable() syntax does not, and the "${var}" and "${expr}" string interpolation forms. All of them still run in 8.2, so you can migrate gradually, but each one is a signal to rewrite.
A sensible upgrade path looks like this:
- Run your test suite on PHP 8.2 with
error_reportingset toE_ALLand deprecations visible, and collect every deprecation message before changing any code. - Fix dynamic properties first. Declaring the property is the preferred fix. Use
#[\AllowDynamicProperties]only for classes that truly need open-ended state, and remember that it also applies to child classes. AWeakMapsuits extra data attached to objects you do not own. - Replace relative callables with first-class callable syntax such as
self::method(...), or with an explicit class name, and rewrite"${var}"as"{$var}". - Only then start using the new type-system features, which help you tighten APIs.
The backward-incompatible list for 8.2 is mostly library and extension behavior, which this page deliberately leaves out. One caution is relevant anywhere you rely on locale-sensitive string functions: they are covered in the separate compatibility notes, so check them alongside the syntax work.
The examples below are written to behave the same on PHP 8.2 and on the newest release tested here, 8.5.10.
DEPRECATED
Dynamic properties are deprecated
Assigning an undeclared property on an object now emits a deprecation unless the class opts in with #[\AllowDynamicProperties].
In PHP 8.1, writing to a property that the class never declared silently created it. In PHP 8.2 this emits a deprecation unless the class opts in with the #[\AllowDynamicProperties] attribute. stdClass still allows dynamic properties, and classes that use __get() and __set() are not affected.
The preferred fix is to declare the property, which also improves static analysis and catches typos. The attribute is a quick escape hatch, but it is inherited by child classes, so apply it deliberately. If you need to attach extra data to an object you do not own, a WeakMap keyed by that object avoids touching the object at all.
Expect the noise to be greatest in older code: hydrators that copy array keys onto objects, mocks, and models whose fields come from a database. The code still runs on 8.2, so you can fix it incrementally, but treat each message as a real bug or design decision. The example declares a property and keeps side data in a WeakMap.
class User {
public function __construct(public string $name) {}
}
$u = new User('Ada');
$u->nickname = 'ada';
echo $u->nickname, "\n";Accepted silently in PHP 8.1; in PHP 8.2 and later it works but emits a deprecation. Shown for comparison; not executed on this site.
final class User {
public ?string $nickname = null;
public function __construct(public string $name) {}
}
$u = new User('Ada');
$u->nickname = 'ada';
$extra = new WeakMap();
$extra[$u] = ['visits' => 2];
echo $u->name, ' ', $u->nickname, ' ', $extra[$u]['visits'], "\n";Ada ada 2
NEW
Readonly classes
The readonly modifier can now be placed on a class, making its declared properties readonly in one step.
PHP 8.1 introduced readonly properties, but you had to mark each one. PHP 8.2 lets you write readonly on the class itself, which applies the restriction to the properties the class declares, including promoted constructor parameters. This is convenient for value objects and immutable data carriers, where repeating the modifier on every property was noisy.
The practical effect is the same as before: a property can be initialized once, from inside the class scope, and any later write throws an Error. Immutable objects therefore need "wither" methods that build a new instance instead of changing the current one.
Pitfalls: readonly properties must be typed, so a readonly class cannot declare untyped properties. The restriction also covers the properties of the class itself, so check how subclasses and any code that relied on mutation will behave before converting an existing class. The example builds a modified copy and shows the error on a direct write.
class Point {
public function __construct(
public readonly int $x,
public readonly int $y,
) {}
}Valid in PHP 8.1 and later; the class-level modifier is not available before 8.2. Shown for comparison; not executed on this site.
readonly class Point {
public function __construct(public int $x, public int $y) {}
public function withX(int $x): static {
return new static($x, $this->y);
}
}
$p = new Point(1, 2);
$q = $p->withX(5);
try {
$p->x = 9;
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
echo $p->x, ',', $q->x, "\n";Cannot modify readonly property Point::$x 1,5
NEW
Disjunctive normal form (DNF) types
Intersection types can now be combined with union types, as long as the type is written in DNF with parentheses around each intersection.
PHP 8.1 had union types (A|B) and intersection types (A&B), but you could not mix them. PHP 8.2 allows combining them when the type is written in disjunctive normal form: a union of terms, where each term is either a single type or a parenthesized intersection, such as (A&B)|null or (A&B)|C.
This lets a signature say that a value must implement several interfaces at once, while still allowing an alternative such as null. Before, you had to drop to a looser type and add instanceof checks, which moved the contract out of the signature and into the body.
The pitfall is the syntax rule: parentheses are mandatory around intersections inside a union, and a form like A&B|C without them is rejected. DNF types also apply to properties and return types, not only parameters. The example accepts an object that is both HasId and HasName, or null.
function describe(mixed $v): string {
if ($v === null) {
return 'none';
}
if (!$v instanceof HasId || !$v instanceof HasName) {
throw new TypeError('bad type');
}
return $v->id() . ':' . $v->name();
}Works in PHP 8.1 and later, but the contract is not visible in the signature. Shown for comparison; not executed on this site.
interface HasId { public function id(): int; }
interface HasName { public function name(): string; }
class Item implements HasId, HasName {
public function id(): int { return 7; }
public function name(): string { return 'box'; }
}
function describe((HasId&HasName)|null $v): string {
return $v === null ? 'none' : $v->id() . ':' . $v->name();
}
echo describe(new Item), ' ', describe(null), "\n";7:box none
DEPRECATED
"${var}" and "${expr}" interpolation deprecated
The "${var}" and "${expr}" string interpolation styles are deprecated in favor of "{$var}" and "{${expr}}".
PHP has several ways to embed variables in double-quoted strings. In PHP 8.2 two of them, "${var}" and "${expr}", are deprecated. The replacements are "$var" or "{$var}" for plain variables, and "{${expr}}" for the expression form.
This matters because the old syntax is easy to find in older code and templates, often written out of habit as "${name}". It still works on PHP 8.2, but now produces a deprecation each time the string is compiled, which can flood logs on a busy codebase.
The mechanical fix for the common case is to move the dollar sign inside the braces: ${name} becomes {$name}. Be more careful with the expression form, where ${expr} treats the result of the expression as a variable name; rewrite it as {${expr}}, and review each case instead of running a blind search-and-replace. The example shows both replacements.
$name = 'Ada';
$var = 'name';
echo "Hello ${name}\n";
echo "Dynamic: ${$var}\n";Accepted in PHP 8.1; still works in PHP 8.2 but emits a deprecation. Shown for comparison; not executed on this site.
$name = 'Ada';
$var = 'name';
echo "Hello {$name}\n";
echo "Also $name\n";
echo "Dynamic: {${$var}}\n";Hello Ada Also Ada Dynamic: Ada
DEPRECATED
Relative callables are deprecated
Callables like "self::method" or ["parent", "method"] that call_user_func() accepts but $callable() does not are deprecated.
Some callable forms are accepted by call_user_func() and by callable type checks, but cannot be invoked with the $callable() syntax. PHP 8.2 deprecates them. The affected forms include the strings "self::method", "parent::method" and "static::method", the arrays ["self", "method"], ["parent", "method"] and ["static", "method"], and forms such as ["Foo", "Bar::method"] and [new Foo, "Bar::method"].
The reason to care is that these callables depend on the calling context to resolve self, parent or static, so passing them to another function can resolve differently than intended. Normal callables such as "A::method" and ["A", "method"] are not affected.
The cleanest replacement is first-class callable syntax, like self::hello(...), which resolves the name where you write it and returns a Closure. Alternatively, use an explicit class name. Search for callables built as strings or arrays and passed to functions like array_map() or usort(). The example maps over a list using a closure created inside the class.
class Greeter {
public static function hello(string $n): string { return "Hello $n"; }
public function run(): array {
return array_map('self::hello', ['Ada', 'Lin']);
}
}Accepted in PHP 8.1; still works in PHP 8.2 but emits a deprecation. Shown for comparison; not executed on this site.
class Greeter {
public static function hello(string $n): string {
return "Hello $n";
}
public function run(): array {
return array_map(self::hello(...), ['Ada', 'Lin']);
}
}
echo implode(', ', (new Greeter)->run()), "\n";Hello Ada, Hello Lin
NEW
The true type
PHP 8.2 adds true as a type that accepts only the value true, complementing the existing false type in unions.
PHP 8.2 adds true as a type declaration. It accepts only the boolean value true, so a function declared with a true return type documents and enforces that it never returns false.
Before, the closest you could get was bool, which allows both values and therefore tells callers nothing. The new type is useful for methods that must satisfy an interface returning bool, or for fluent-style APIs where success is the only outcome and failure is signalled by an exception. It also pairs naturally with unions such as int|true.
A few cautions: this is a type for the literal value true, not a general truthiness check, and returning anything else is a TypeError at runtime or a compile-time error for a literal false. Because the type only exists from PHP 8.2, code that must also run on 8.1 has to keep using bool. The example declares a function with a true return type.
function enable(): bool {
return true;
}Valid in PHP 8.1 and later, but the signature cannot say the result is always true. Shown for comparison; not executed on this site.
function enable(): true {
return true;
}
var_dump(enable());bool(true)
NEW
null and false as stand-alone types
null and false can now be used alone as parameter, return, and property types, not just inside unions.
In PHP 8.1, null and false were only usable as part of a union type, such as int|false or string|null. PHP 8.2 allows each to be declared on its own, for example function f(): null or function g(): false.
This fills a gap that mattered mostly for precise signatures: an interface method or overriding method that always returns null or always returns false can now say so. It also helps when a subclass narrows a broader union return type to one of its members.
There are limits to keep in mind. These are narrow types, so they are not a replacement for void when a function returns nothing; a null return type still requires an explicit return null;. Likewise, a false-typed function must return false, not any falsy value. As with the true type, code that must run on 8.1 cannot use them. The example defines one method of each kind and prints what they return.
class Config {
public function missing(): ?string { return null; }
public function failed(): int|false { return false; }
}Valid in PHP 8.1 and later; the narrow stand-alone forms need PHP 8.2. Shown for comparison; not executed on this site.
class Config {
public function missing(): null { return null; }
public function failed(): false { return false; }
}
$c = new Config();
var_dump($c->missing(), $c->failed());NULL bool(false)
NEW
Constants in traits
Traits can now declare constants, which become available on the classes that use the trait.
Before PHP 8.2, a trait could hold methods and properties but not constants. You had to put shared values in an interface, a base class, or repeat them in every class. PHP 8.2 lets a trait declare constants directly with const, and they become constants of each class that uses the trait.
This keeps a shared value next to the trait code that uses it. Methods in the trait can read the constant through self::, and consumers can read it through their own class name.
Keep two things in mind. The constant belongs to the using class, so you reach it as Api::VERSION, not through the trait name, which is not an instantiable type and cannot be used to read the constant. Also, if two traits define a constant of the same name, or a class defines one that conflicts, the usual conflict rules apply and you will get an error rather than a silent override. The example reads the constant both from the class and from a trait method.
trait HasVersion {
public function version(): string {
return '1.2';
}
}PHP 8.1 does not allow constants in traits, so a method or a base class was needed. Shown for comparison; not executed on this site.
trait HasVersion {
public const VERSION = '1.2';
public function version(): string {
return self::VERSION;
}
}
class Api {
use HasVersion;
}
echo Api::VERSION, ' ', (new Api())->version(), "\n";1.2 1.2
NEW
Enum properties in constant expressions
You can now read an enum case's properties, such as ->value, inside constant expressions like class constants.
Backed enums expose name and value properties on each case. In PHP 8.1 you could not use those properties in a constant expression, so a class constant could not be defined as Status::Active->value. You had to repeat the literal string or compute it at runtime.
PHP 8.2 allows fetching enum properties in constant expressions. That means class constants, array keys in constants, and other compile-time contexts can refer to the enum instead of duplicating its backing values. The benefit is a single source of truth: renaming a backing value in the enum updates every constant that derives from it.
The feature covers reading properties of enum cases; it does not make arbitrary object property access valid in constant expressions. It also needs a backed enum if you want ->value. The example builds a default constant and a label map keyed by backing values.
enum Status: string { case Active = 'active'; case Off = 'off'; }
class Account {
public const DEFAULT_STATUS = 'active';
}PHP 8.1 rejects Status::Active->value in a constant expression, so the literal was duplicated. Shown for comparison; not executed on this site.
enum Status: string {
case Active = 'active';
case Off = 'off';
}
class Account {
public const DEFAULT_STATUS = Status::Active->value;
public const LABELS = [
Status::Active->value => 'On',
Status::Off->value => 'Off',
];
}
echo Account::DEFAULT_STATUS, ' ', Account::LABELS['active'], "\n";active On