PHP 8.2 Is Coming: Readonly Classes and New Types
A look at readonly classes, DNF types, and standalone type declarations in PHP 8.2, and their impact on domain modeling.
I’m expecting PHP 8.2 to ship by the end of the year. I’m writing this before the release, after going through the RFCs and the beta package — because 8.2, modest as its headlines may seem, makes something meaningful easier for me: expressing immutability at the class level.
Readonly classes
PHP 8.1 introduced readonly properties: you could assign a property exactly once, inside the constructor. It worked, but on a value object with ten fields, writing readonly in front of every single one created a lot of noise.
With 8.2, you can declare the entire class as readonly:
readonly class Money
{
public function __construct(
public int $amount,
public string $currency = 'USD',
) {}
}
All properties in that class automatically become readonly. Once a Money object is constructed, it cannot be mutated; anyone who wants a different value creates a new instance. For years I’ve been manually enforcing “this is immutable” on value objects; now the language provides that guarantee.
One constraint with readonly classes
Readonly classes have one restriction: every property must have an explicit type, including standalone types. You cannot apply readonly to an untyped property, and the same rule holds for readonly classes. So writing public $field will cause a compilation error; you need public mixed $field instead.
This may seem like a minor limitation, but it can surface when migrating old value objects — written back when type hints were optional — to readonly classes. When planning a migration, it’s worth auditing every property’s type declaration.
DNF types
The second addition is Disjunctive Normal Form (DNF) types. Until now, we could use A&B (intersection) and A|B (union) types separately, but not together. With 8.2, you can combine them:
function save((Countable&Iterator)|null $data): void
Here, the parameter is either an object that satisfies both the Countable and Iterator interfaces, or null. You won’t need this every day, but at library boundaries it lets you keep type signatures honest.
The scenario where I wanted this most was utility functions that process object collections. I wanted a type that means “both traversable and countable” — writing Traversable&Countable in the type signature wasn’t cleanly possible before 8.2.
Standalone null, true, false
Previously, null couldn’t be written as a return type on its own; you had to embed it inside a ?something. With 8.2, null, true, and false become independent types:
function find(int $id): User|null { /* ... */ }
function isActive(): true { /* ... */ }
A small fix, but it lets the type signature communicate intent precisely.
Deprecation of dynamic properties
PHP 8.2 also marks assigning a value to an undeclared property — a dynamic property — with a deprecation warning. Over the years I’ve spent hours tracking down bugs caused by a line like $object->typoedFieldName = 1. Closing off this behavior forces classes to explicitly declare what they hold, which is healthy pressure.
On legacy codebases, encountering these warnings is inevitable; some code may intentionally rely on dynamic property assignment. The #[AllowDynamicProperties] attribute offers a temporary escape hatch for the migration. But the permanent fix is to declare properties explicitly.
What this all adds up to
The common thread running through these additions is: the language is opening up more room to express intent. A readonly class says “this object is immutable after construction”; DNF types say “this parameter is exactly this shape”; the removal of dynamic properties says “this class holds only these things.”
Changes like these shift how I build structures in PHP. After 8.1’s enums and readonly properties, the readonly class in 8.2 closes the gap between language and intent one step further. As soon as the release drops, I plan to migrate my value objects to readonly classes. It’s not a massive migration — but I enjoy the idea that rules I previously enforced through comments or team discipline will now be enforced by the language itself. A good release is one that guarantees more while requiring less code to do it.
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.