readonly and __toString
Some data is set once and for good. A passport number is not changed while the passport is valid: if another number is needed, a new passport is issued.
A house has the same kind of thing—a number plate carved in stone when the house was built. It cannot be rewritten; you can only build another house with another plate.
Closing a property with the word private and giving it a getter is one way. But then the class's own methods can still change it by accident, and every property needs its own getter. Since PHP 8.1 there is a shorter and stricter way—readonly.
readonly: written once
<?php
class Passport
{
public function __construct(
public readonly string $number,
public readonly string $owner,
) {}
}
$p = new Passport('4510 123456', 'Anna');
echo $p->owner, ' ', $p->number, "\n"; // Anna 4510 123456Let's go through it step by step:
public readonly string $numberis a promoted property with the wordreadonly.new Passport('4510 123456', 'Anna')writes values into the properties: this is the first and only write.- Such properties can be read from anywhere, and
echoprintsAnna 4510 123456.
A readonly property is a property that gets its value once, usually in the constructor, and after that can only be read. Let's try to change the owner:
$p->owner = 'Olga'; // ✗Fatal error: Uncaught Error: Cannot modify readonly property Passport::$ownerThis is an error, not a warning: the lines above have run, and the program stops at this one. The ban holds for the class's own methods too: there is no second write, from outside or from inside.
The rules for readonly:
- such a property must have a type, or the file will not run;
- a default value in the property declaration is not allowed, but a promoted parameter may have one:
public readonly int $level = 1in the constructor works; - since PHP 8.2 you can declare a
readonly class, and then every property of the class becomesreadonly.
A new value means a new object
Nobody rewrites the plate on a house; they build a new house. It is the same with readonly: a passport with another owner is a new object, new Passport('4510 654321', 'Olga'), and the old one stays as it was.
A copy made with clone won't help here: it gets the properties already written, and a write to it throws the same error. For such cases PHP 8.5 added the function clone() with a list of new values. Inside the class, for example in a method withOwner(), you write clone($this, ['owner' => $owner]).
Here the stone picture is off: readonly only forbids writing something else into the property. If the property holds an object, that object's own properties can still change.
Interviewers ask why readonly is needed. The answer: a variable holds an object handle, and any code that got the handle could change the object. An object with readonly properties can be handed anywhere: nobody will overwrite its data.
__toString: an object in echo
echo can print numbers and strings, but not objects. To make an object printable, the class declares the method __toString:
<?php
class Price
{
public function __construct(public readonly int $cents) {}
public function __toString(): string
{
return number_format($this->cents / 100, 2) . ' EUR';
}
}
$price = new Price(1250);
echo $price, "\n"; // 12.50 EUR
echo 'Total: ' . $price, "\n"; // Total: 12.50 EURLet's take it apart:
__toString(): stringreturns the string the object will be shown as.echo $pricecalls__toStringby itself and prints12.50 EUR.- Joining with
.does the same: the object turns into a string.
A magic method is a method with two underscores at the start of its name that PHP calls by itself. You have already met __construct and __clone, and __toString is the third of them.
Common mistake
An object without __toString is printed with echo:
echo $p, "\n"; // ✗Fatal error: Uncaught Error: Object of class Passport could not be converted to stringPHP does not know which string to show the passport as, and it stops the program. The fix is to declare __toString in the class or to print the properties you need: echo $p->owner, "\n";.
Key points
- A
readonlyproperty gets its value once, and any later write is anError. - A
readonlyproperty must have a type and has no default value in its declaration, and areadonly classmakes every property like that. - To "change" a
readonlyproperty, you create a new object. __toStringsets the string an object prints as inecho; without it,echoof an object is an error.