PHP 8.1 adds support for a readonly modifier on class properties. A property that’s tagged in this way can only be set once. Trying to change a readonly property’s value after initialization will throw an error.
“Readonly” is quite a vague term, with varying implications in individual programming languages. In this context, “readonly” really means “immutable”—you can set a property’s value, but it can’t be changed afterward.
Writing Readonly Properties
Add the readonly modifier to make a property readonly. It should be placed between the property’s access modifier and its typehint.
$Mutable is a regular public property. You can change its value at any time, either within class methods or from outside:
$Immutable is a little different. You can still read its value whenever you want, but any modifications will fail with an Error:
The readonly modifier is also supported in promoted constructor properties:
You can still set the property manually in your constructor when you’re using promotion with a default value. It’s the function parameter that receives the default value, not the property instance. The promotion desugars to the same code shown in the earlier example.
Caveats
Readonly properties are a straightforward syntax enhancement that you can adopt at your leisure. There are no backward compatibility implications, and their usage is entirely optional. If you do start to add them, there are a few caveats and limitations to be aware of.
Unlike regular properties, readonly properties aren’t allowed to have a default value in their definition:
This statement defines and initializes $foo. You can’t change its value after initialization, so the property is effectively a constant. As a result, default values are banned, and you should use a real constant instead:
Following on from this restriction, readonly is only supported on typed properties. The following property has an illegal definition:
An untyped property, like $foo above, has an implicit default value of null. If readonly was allowed, the “no implicit constants” rule would resurface. Typed properties distinguish between “uninitialized” and “null” states, so they exist without any value until you explicitly set one.
The readonly modifier has special rules when used as part of class inheritance. Child classes can’t add or remove readonly on properties defined by their parents.
Making a writable property readonly would break the parent class if its methods mutated the value. While removing the constraint is seemingly innocuous, the RFC views readonly as an “intentional restriction” of capabilities that would be lost if inheritance overrides were allowed. It’s forbidden so that parent classes can assert that children can’t cause side effects by modifying properties that are meant to be readonly.
Readonly properties can only be set within the scope in which they’re defined. This means that public properties can’t be set from outside a class, even if they haven’t been previously initialized:
Initialization must occur within the class that defines the property. As a result, readonly properties are closer to the immutable fields of other programming languages as opposed to pre-existing PHP properties.
The readonly modifier applies equally to all writes. It doesn’t distinguish between internal and external access. You can’t have a property that’s publicly readonly but writable within the class, although a future spec extension could allow it.
One final gotcha concerns the clone keyword. This code won’t work:
Cloning follows the same rules as regular property accesses. Even though the change to foobar is the first time that foo is accessed on $d2, the property has already been initialized by the cloning process. There’s an implicit initialization during the clone.
When to Use Readonly Properties?
Readonly properties will significantly accelerate the creation of simple classes, which represent data structures. It’s common to write throwaway classes to hold HTTP request parameters, data transfer objects, and response data. These are typically immutable, where properties aren’t expected to change after the class is constructed.
You previously had two unappealing choices when writing struct-like classes: use public properties, speeding up development but allowing modification, or spend time manually adding getter methods to expose protected properties.
Readonly properties finally make the ideal approach possible:
The properties are publicly accessible but immutable. Combined with constructor property promotion, readonly properties promise to significantly slim down boilerplate code, letting you write useful classes more quickly.
readonly also aids code readability and better indicates your intentions. Anyone reading or editing the UserCreationRequest class knows that its properties aren’t meant to change. In the first example, it’s unknown whether other code in the project modifies the class properties directly. The second example is a little clearer, but could still induce a developer to implement redundant setUsername() and setPassword() methods.
Conclusion
The readonly modifier brings built-in immutability support to PHP class properties. It makes your code clearer and prevents unintentional value changes by enforcing immutability at runtime.
Looking back to the PHP 7 release series, creating a basic struct-like class used to involve defining typed properties, writing a constructor that set those properties, and then adding getter methods to expose their values. With PHP 8.1, you can condense all that down to a single constructor signature by combining public readonly and property promotion.
Readonly properties are implemented in the latest PHP 8.1 development builds. The production-ready release will arrive in November of 2021.