readonly и __toString
Некоторые данные задаются один раз и навсегда. Номер паспорта не меняют, пока паспорт действует: если нужен другой номер, выдают новый паспорт.
У дома есть такая же вещь — табличка с номером, высеченная в камне при постройке. Переписать её нельзя, можно только построить другой дом с другой табличкой.
Закрыть свойство словом private и дать к нему геттер — один способ. Но тогда методы самого класса могут случайно его поменять, а каждому свойству нужен свой геттер. С PHP 8.1 есть способ короче и строже — readonly.
readonly: запись один раз
<?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 123456Разберём по шагам:
public readonly string $number— продвинутое свойство со словомreadonly.new Passport('4510 123456', 'Anna')записывает в свойства значения: это первая и единственная запись.- Читать такие свойства можно откуда угодно, и
echoпечатаетAnna 4510 123456.
Свойство readonly — свойство, которое получает значение один раз, обычно в конструкторе, а дальше его можно только читать. Попробуем сменить владельца:
$p->owner = 'Olga'; // ✗Fatal error: Uncaught Error: Cannot modify readonly property Passport::$ownerЭто ошибка Error: строки выше выполнились, а на этой программа останавливается. Запрет действует и для методов самого класса: второй записи не будет ни снаружи, ни изнутри.
Правила для readonly:
- у такого свойства обязательно есть тип, без него файл не запустится;
- значение по умолчанию в объявлении свойства запрещено, а у продвинутого параметра оно допустимо:
public readonly int $level = 1в конструкторе работает; - с PHP 8.2 можно объявить
readonly class, и тогда все свойства класса станутreadonly.
Новое значение — новый объект
Табличку на доме не переписывают, а строят новый дом. С readonly так же: паспорт с другим владельцем — это новый объект, new Passport('4510 654321', 'Olga'), а старый остаётся прежним.
Копия через clone здесь не поможет: она получает свойства уже записанными, и запись в неё бросает ту же ошибку. Для таких случаев в PHP 8.5 появилась функция clone() со списком новых значений. Внутри класса, например в методе withOwner(), пишут clone($this, ['owner' => $owner]).
Здесь картинка с камнем неточна: readonly запрещает только записать в свойство новое значение. Если в свойстве лежит объект, его собственные свойства менять можно.
На собеседовании спрашивают, зачем нужен readonly. Ответ: переменная хранит идентификатор объекта, и любой код, получивший его, мог бы объект изменить. Объект со свойствами readonly можно отдавать куда угодно: никто не перепишет его данные.
__toString: объект в echo
echo умеет печатать числа и строки, но не объекты. Чтобы объект можно было напечатать, в классе объявляют метод __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 EURДавайте разберём:
__toString(): stringвозвращает строку, которуюechoпечатает вместо объекта.echo $priceсам вызывает__toStringи печатает12.50 EUR.- При склейке через
.происходит то же самое: объект превращается в строку.
Магический метод — метод с двумя подчёркиваниями в начале имени, который PHP вызывает сам. Вы уже встречали __construct и __clone, а __toString — третий из них.
Частая ошибка
Объект без __toString печатают через echo:
echo $p, "\n"; // ✗Fatal error: Uncaught Error: Object of class Passport could not be converted to stringPHP не знает, какой строкой показать паспорт, и останавливает программу. Исправление — объявить в классе __toString или печатать нужные свойства: echo $p->owner, "\n";.
Главное
- Свойство
readonlyполучает значение один раз, и любая следующая запись — ошибкаError. - У
readonlyобязателен тип и нет значения по умолчанию в объявлении, аreadonly classделает такими все свойства. - Чтобы «изменить» свойство
readonly, создают новый объект. __toStringзадаёт, какой строкой объект печатается вecho; без негоechoобъекта — ошибка.