PHP 8.2: readonly Classes
Following the readonly feature in PHP 8.1 for class properties, PHP 8.2 supports declaring an entire class as readonly.
A class declared as readonly automatically makes all properties readonly. PHP 8.2 deprecates dynamic properties, but readonly classes hard-fail dynamic properties with an Error exception.
readonly Class Syntax
readonly classes are declared with the readonly keyword before the class declaration:
readonly class MyValueObject {
public string $myValue;
}
Abstract classes final classes can also be declared readonly. The order of the keywords does not make a difference.
abstract readonly class Foo {}
final readonly class Bar {}
It is possible to declare a readonly class with no properties in them, which effectively prevents dynamic properties while allowing child classes to explicitly declare their readonly properties.
As it is the case for all PHP keywords, the readonly keyword is case insensitive.
- Enums cannot contain properties at all, and it is not allowed to declare enums as
readonly. - Traits cannot be declared
readonly. - Interfaces cannot be declared
readonly.
Attempting to declare Enums, traits, and Interfaces as readonly results in a Parse error:
readonly interface Baz {}
Parse error: syntax error, unexpected token "interface", expecting "abstract" or "final" or "readonly" or "class" in ... on line ...
readonly Semantics
When a class is declared readonly, the entirety of the class and its declared members are considered readonly. This follows exactly the same semantics of readonly properties. To summarize, a readonly property:
- Only be initialized from within the class scope.
- Cannot be modified once initialized.
- Cannot be
unsetonce initialized. - Must be a typed property.
When a class is declared readonly, that class:
- Must only contain typed properties
- Must not use dynamic properties
- Must not use
#[AllowDynamicProperties]attribute - Cannot opt-out of the read-only status
When a readonly class is extended by a subclass, that class:
readonly classes must only contain typed properties
All properties of a readonly class must be typed properties. For properties that cannot be strictly typed, consider using the mixed type. This rule is the same as that individual readonly properties must be typed properties.
Attempting to declare a readonly class property without a type causes a Fatal error:
readonly class Test {
public $test;
}
Fatal error: Readonly property Test::$test must have type in ... on line ...
readonly classes must not use dynamic properties
PHP 8.2 deprecates dynamic properties unless the class declares __get()/__set() methods or attributed with the AllowDynamicProperties attribute.
A readonly class must not use dynamic properties. Attempting to set a dynamic property results in an Error exception:
readonly class Test {
public string $test;
}
$t = new Test();
$t->test2 = 'Hello';
Error: Cannot create dynamic property Test::$test2 in ...:...
readonly classes must not use #[AllowDynamicProperties] attribute
AllowDynamicProperties attribute, introduced in PHP 8.2 can be used to explicitly opt-in for dynamic properties.
Because dynamic properties are not allowed in readonly classes, using AllowDynamicProperties attribute is not allowed, and results in a Fatal error.
#[AllowDynamicProperties]
readonly class Test {}
Fatal error: Cannot apply #[AllowDynamicProperties] to readonly class Test in ... on line ...
readonly in Subclasses
Declaring a class readonly does not prevent another class from extending the readonly class. However, the subclass must not break the semantics of the parent readonly class.
readonly subclass must also be declared readonly
The child class must also explicitly declare that the class is readonly. Note that when a parent class is provided by a different library/framework, declaring the parent class as readonly will be effectively a backwards-compatibility break.
readonly class Test{}
readonly class SubTest extends Test {}
Attempting to declare a subclass extending a readonly parent class without the readonly keyword results in a Fatal error:
readonly class Test{}
class SubTest extends Test {}
Fatal error: Non-readonly class SubTest cannot extend readonly class Test in ... on line ...
Opting-out of read-only status
Once a class is declared readonly, it is not possible to opt-out. This applies to subclasses as well, because subclasses must also explicitly be declared readonly.
Mutability
Note that readonly classes do not provide complete immutability. While readonly classes are ideal for value-objects that guarantee their data does not change, objects stored in a readonly property can change. This is the exact same behavior as readonly properties.
Reflection API Changes
The ReflectionClass class provides a new ReflectionClass::isReadOnly() method that returns a boolean value indicating whether the class being reflected is declared readonly.
readonly class MyValueObject {
public string $myValue;
}
$reflection = new ReflectionClass('MyValueObject');
$reflection->isReadOnly(); // true
Further, there is a new constant ReflectionClass::IS_READONLY, assigned 65536.
ReflectionClass::getModifiers method returns a bitmask that reveals existence of the readonly flag:
final readonly class MyValueObject {
public string $myValue;
}
$reflection = new ReflectionClass('MyValueObject');
$modifiers = $reflection->getModifiers();
($modifiers & ReflectionClass::IS_READONLY) === ReflectionClass::IS_READONLY; // true
Backwards Compatibility Impact
The readonly class syntax is new in PHP 8.2, and classes that are declared readonly result in a Parse error in older PHP versions.
As a stop-gap for readonly classes, consider declaring all properties as readonly, which is supported since PHP 8.1.