Перейти к содержимому
Теория · Классы и объекты · урок 10 из 11

readonly и __toString

Некоторые данные задаются один раз и навсегда. Номер паспорта не меняют, пока паспорт действует: если нужен другой номер, выдают новый паспорт.

У дома есть такая же вещь — табличка с номером, высеченная в камне при постройке. Переписать её нельзя, можно только построить другой дом с другой табличкой.

Закрыть свойство словом private и дать к нему геттер — один способ. Но тогда методы самого класса могут случайно его поменять, а каждому свойству нужен свой геттер. С PHP 8.1 есть способ короче и строже — readonly.

readonly: запись один раз

PHP
<?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

Разберём по шагам:

  1. public readonly string $number — продвинутое свойство со словом readonly.
  2. new Passport('4510 123456', 'Anna') записывает в свойства значения: это первая и единственная запись.
  3. Читать такие свойства можно откуда угодно, и echo печатает Anna 4510 123456.

Свойство readonly — свойство, которое получает значение один раз, обычно в конструкторе, а дальше его можно только читать. Попробуем сменить владельца:

PHP
$p->owner = 'Olga'; // ✗
TEXT
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
<?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

Давайте разберём:

  1. __toString(): string возвращает строку, которую echo печатает вместо объекта.
  2. echo $price сам вызывает __toString и печатает 12.50 EUR.
  3. При склейке через . происходит то же самое: объект превращается в строку.

Магический метод — метод с двумя подчёркиваниями в начале имени, который PHP вызывает сам. Вы уже встречали __construct и __clone, а __toString — третий из них.

Частая ошибка

Объект без __toString печатают через echo:

PHP
echo $p, "\n"; // ✗
TEXT
Fatal error: Uncaught Error: Object of class Passport could not be converted to string

PHP не знает, какой строкой показать паспорт, и останавливает программу. Исправление — объявить в классе __toString или печатать нужные свойства: echo $p->owner, "\n";.

Главное

  • Свойство readonly получает значение один раз, и любая следующая запись — ошибка Error.
  • У readonly обязателен тип и нет значения по умолчанию в объявлении, а readonly class делает такими все свойства.
  • Чтобы «изменить» свойство readonly, создают новый объект.
  • __toString задаёт, какой строкой объект печатается в echo; без него echo объекта — ошибка.