PHPMiddleTechnical

Что такое readonly properties и классы в PHP 8.1/8.2?

Readonly properties (PHP 8.1) можно инициализировать один раз; повторная запись вызывает Error. Readonly classes (PHP 8.2) делают все свойства readonly автоматически, обеспечивая иммутабельные объекты.

Readonly properties (PHP 8.1)

Свойство с модификатором readonly можно присвоить ровно один раз — обычно в конструкторе. Любая последующая запись бросает Error: Cannot modify readonly property.

<?php

class Money
{
    public readonly int $amount;
    public readonly string $currency;

    public function __construct(int $amount, string $currency)
    {
        if ($amount < 0) {
            throw new InvalidArgumentException('Amount cannot be negative');
        }
        $this->amount   = $amount;
        $this->currency = strtoupper($currency);
    }
}

$price = new Money(1000, 'usd');
echo $price->amount;   // 1000
echo $price->currency; // USD

// $price->amount = 2000; // Error: Cannot modify readonly property Money::$amount

Constructor property promotion + readonly

<?php

class UserCreatedEvent
{
    public function __construct(
        public readonly int    $userId,
        public readonly string $email,
        public readonly \DateTimeImmutable $occurredAt = new \DateTimeImmutable(),
    ) {}
}

$event = new UserCreatedEvent(userId: 42, email: 'alice@example.com');
echo $event->userId; // 42

Readonly classes (PHP 8.2)

Ключевое слово readonly перед class делает все объявленные свойства readonly автоматически. Нельзя добавлять нетипизированные свойства и динамические свойства.

<?php

readonly class Point
{
    public function __construct(
        public float $x,
        public float $y,
        public float $z = 0.0,
    ) {}

    // Иммутабельный «вither»
    public function withX(float $x): static
    {
        return new static($x, $this->y, $this->z);
    }
}

$p1 = new Point(1.0, 2.0);
$p2 = $p1->withX(5.0);

echo $p1->x; // 1.0 — неизменён
echo $p2->x; // 5.0 — новый объект

Клонирование readonly объектов (PHP 8.3)

В PHP 8.3 clone with позволяет создавать изменённую копию readonly-объекта:

<?php

$p3 = clone $p1 with { x: 9.0 };
echo $p3->x; // 9.0
echo $p3->y; // 2.0 — скопировано

Ограничения

  • Readonly свойства должны иметь явный тип — public readonly $x вызывает синтаксическую ошибку.
  • Нельзя использовать unset() для readonly свойства после инициализации.
  • Readonly class не может содержать статические свойства.
  • Дочерний класс readonly class тоже должен быть объявлен readonly.

Подводные камни

  • Попытка десериализации (unserialize) readonly-объекта: __unserialize() пытается записать в свойства после конструктора — нужен явный __unserialize через reflection или специальный обходной путь.
  • Тестирование: создать mock или stub с readonly-свойствами через PHPUnit невозможно стандартными методами; нужны фабрики или клонирование.
  • Гидрация из БД (Doctrine/Eloquent): ORM создаёт объект без конструктора и присваивает свойства напрямую через Reflection — это работает до инициализации, но нарушает семантику readonly при повторной гидрации.
  • Readonly class нельзя использовать как mutable DTO: при изменении одного поля придётся пересоздавать весь объект.
  • Статические анализаторы (PHPStan/Psalm) могут ложно сообщать об ошибках при использовании readonly с Reflection API.

Common mistakes

  • Сводить readonly properties classes к названию метода без lifecycle и failure path.
  • Игнорировать модель runtime: интерпретируемый runtime PHP 8.x, обычно запускаемый как отдельный запрос в FPM, CLI или worker-процессе.
  • Не отделять validation, authorization, transaction boundary и business logic.

What the interviewer is testing

  • Объясняет readonly properties classes через конкретную точку lifecycle в PHP.
  • Приводит корректный минимальный пример без вымышленных методов или callbacks.
  • Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.

Sources

Related topics