
by Alex
PHP 8.1 introduced the concept of readonly class properties:
class MyClass {
public readonly string $name;
}
If you declare a property as readonly, then you can assign a value to that property only once.
If you try to change its value again, a fatal error occurs.
In other words, after you initialize a readonly property you cannot change its value anymore.
For example:
class MyClass {
public readonly string $name;
public function setName($newName) {
$this->name = $newName;
}
}
$my = new MyClass();
$my->setName('Alex'); // -> First $name initialization, no problems here.
$my->setName('Jim'); // -> FATAL ERROR: Cannot modify readonly property myclass::$name
Readonly properties must also follow these rules:
- They must be typed.
- They cannot be static.
- They can only be set from a method of their class, even if they are public.
- Once set, they cannot be unset.
As all PHP typed properties, you cannot access them before they are initialized.
When to use readonly properties: 3 examples.
The main purpose of the readonly attribute is to enforce some rules over the property.
This makes the code more stable, because it prevents bugs that would break the readonly rules. It also makes the code more readable, giving more information about the property.
There are many cases where you can use readonly properties, including:
- Properties that represent “IDs”
- Dependencies such as database objects (PDO, mysqli…) or file handlers
- Resource identifiers such as file names, database tables, or remote servers
- Properties representing a “state” that must not change for consistency, like an $admin state
Let’s look at a few examples.
Example #1: ID properties.
Let’s say that you have an “Account” class where you define an $id property. $id contains the account’s unique id.
Now, let’s see if the $id property should follow the rules required by the readonly attribute:
- It must be initialized before we can use it: sounds right, because we need a valid $id for our Account object to be operational. We do not want to have an unset $id in our object.
- It must not be writeable from outside the class: this too makes sense, because when we set it we want to apply proper validation to make sure it is a valid id.
- It must be immutable, that is, once set it must not change: this is great, because once we have our Account object associated with a specific ID we do not want that ID to change.
All the operations performed by an Account object are related to the specific account ID. If you change that ID, you can easily introduce inconsistencies in your code logic.
If you need to change ID, what you actually need is a new object.
The readonly keyword enforces all of the above rules so you can go on and use it for $id;
class Account {
public readonly int $id;
public function __construct(int $id) {
$this->id = $id;
}
}
Example #2: database dependencies.
If you use dependency injection for the database connection resource, you want the database resource to stay consistent throughout the object’s life.
Again, you can see how using readonly for the resource property is useful:
- It prevents it from being used when not ready (un-initialized)
- It cannot change, which would cause problems with database consistency
Let’s add a private, readonly PDO property to our Account class:
class Account {
private readonly PDO $pdo;
public readonly int $id;
public function __construct(PDO $pdo, int $id) {
$this->pdo = $pdo;
$this->id = $id;
}
}
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$id = 1;
$account = new Account($pdo, $id);
Example #3: “state” properties.
Some properties represent a particular “state” of the object. Some of these states should never change throughout the life of the object.
For example, you can include an $admin property to the Account class representing the “admin state” of the account.
Using readonly for this property makes perfect sense, because you don’t want this value to change. This specific account is either an admin or not.
Depending on whether admin is true or false, your PHP script performs different operations such as displaying a different HTML output and authorizing different operations.
Imagine if, as you are building the HTML page or performing an operation, the admin value changed. That would definitely create inconsistencies.
An objection?
Looking at example #3, some may argue that $admin could indeed change for valid reasons. For example, what if you actually want to promote the account to admin?
This objection is legit. However, it’s important to consider both the benefits and the potential issues of using readonly.
There are cases where $admin needs to change, but they are very specific and it’s easy to handle them in such a way that you don’t actually need to change the $admin property.
For example, after promoting the current account to admin, you can force a page reload (and therefore a new Account object will be created) or explicitly destroy and recreate the Account object.
This may seem excessive, but it’s not really different from having to destroy and recreate the Account object if you want to switch Accounts (instead of changing the current Account’s $id property).
That said, the way you handle the accounts’ admin state may or may not make it not reasonable to use readonly for the admin state.
The readonly attribute helps by making your code more stable and reliable. If that’s worth the limitations it’s up to you to decide.
At the end of the day, it all comes down to balancing the the class’ mutability with the class’ reliability.
Readonly properties and class inheritance.
When you define a child class, you are allowed to override readonly properties from the parent class. However, you must keep the readonly attribute in the child class as well.
Similarly, you are not allowed to add the readonly attribute to overridden properties that don’t have it in the parent class.
For example:
class A {
public int $a;
public readonly int $b;
}
class B extends A {
public readonly int $a; // ERROR: $a is NOT readonly in the parent class.
public int $b; // ERROR: $b IS readonly in the parent class.
}
Note that readonly properties can only be initialized from their own class.
In other words, you cannot initialize readonly properties of a parent class:
class A {
public int $a;
public readonly int $b;
}
class B extends A {
public function __construct() {
$this->a = 1; // OK, because A::$a is not readonly.
$this->b = 2; // ERROR, because A::$b is readonly.
}
}
class B extends A {
public readonly int $b;
public function __construct() {
$this->a = 1; // OK, because A::$a is not readonly.
$this->b = 2; // OK now, because $b has been overridden.
}
}
What I do like about readonly properties.
In my opinion, the readonly attribute is a nice new tool for PHP developers who want to improve the quality of their code. It helps enforce both readability and reliability.
It’s important to remember that the readonly attribute is meant to limit your possibilities. This is a good thing, because less possibilities means less bugs and more clarity.
Indeed, PHP language developers explicitly chose to enforce some limits on readonly properties for the sake of consistency.
For example, here’s an extract from the readonly wiki page about inheritance, where we can read how the developers gave more importance to the concept of readonly as a restriction instead of only focusing on the language semantics:
“It is obvious that overriding a readwrite property with a readonly property needs to be forbidden, because that may render operations performed in the parent class invalid. However, this proposal views readonly not just as a lack of capabilities (which would be safe to increase in a child class), but as an intentional restriction. Lifting the restriction in the child class could break invariants in the parent class. As such, a readonly modifier may be neither added nor removed during inheritance.”
The same applies to the choice of not allowing static readonly properties, as they would be too confusing for developers (as well as providing implementation difficulties):
“Readonly static properties are not supported. This is a technical limitation […] In conjunction with the questionable usefulness of readonly static properties, this is not considered worthwhile at this time.”
Similarly, I like the fact that assignment from outside of the class is not allowed (like for private properties, but reading is still allowed with public).
At the same time, no useless restrictions are enforced (except for one I’m discussing in the next chapter).
Some could wonder why readonly properties must be typed. I think this is a good choice too. For three reasons:
- It isn’t really a restriction because you are free to use the mixed type.
- It is coherent with the fact that readonly helps code readability: typing does it too.
- It forces developers to initialize readonly properties before they are used. While I have some doubts about this requirement in general, in the specific case of readonly properties it does make sense. “Readonly” means that the value of the property influences the application’s logic (think about the previous use cases), and you don’t want this value to be left blank.
My doubts about readonly properties.
There are a couple things about readonly properties that I don’t completely agree with.
First, I don’t really see why initialization from extended classes is not allowed.
I suspect there are technical reasons for this. Quoting from the wiki, it seems that this is done to prevent initialization from outside of the class altogether:
“A readonly property can only be initialized once, and only from the scope where it has been declared. Any other assignment or modification of the property will result in an Error exception.”
Whatever the technical reason, I think children classes could benefit from having write access to the parents’ readonly properties without having to override them.
What do you think about this?
The other fact I don’t completely agree with is the unsetting of uninitialized readonly properties.
This operation is allowed to make it possible to use magic methods and lazy initialization. I don’t see any real-case scenarios where this would be useful, though.
If you have some thought about this please share it in the comments.
That’s it for today. Let me know what you think.
Please leave a comment below if you have any questions or doubts.
And if you are new to my blog, make sure to join Tags to get my free PHP newsletter!