Как гарантировать иммутабельность класса, если у него есть публичное свойство?

«Как гарантировать иммутабельность класса, если у него есть публичное свойство?» — вопрос из категории ООП, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В PHP публичное свойство по умолчанию мутабельно, поэтому для создания иммутабельного класса его нужно защитить от изменений после создания объекта. Вот как я это делаю:

1. Использование readonly-свойств (PHP 8.1+): Это самый чистый и современный способ. Свойство можно объявить публичным, но readonly.

class ImmutablePoint {
    public function __construct(
        public readonly float $x,
        public readonly float $y
    ) {}

    // Методы могут возвращать новые экземпляры, а не менять текущий
    public function translate(float $dx, float $dy): self {
        return new self($this->x + $dx, $this->y + $dy);
    }
}

// Использование
$point = new ImmutablePoint(1.5, 2.0);
echo $point->x; // OK: чтение
// $point->x = 3.0; // ФАТАЛЬНАЯ ОШИБКА: нельзя изменить readonly-свойство
$newPoint = $point->translate(1, 1); // Создаем новый объект

2. Классический способ с приватными свойствами и геттерами (для PHP < 8.1):

class ImmutablePoint {
    private float $x;
    private float $y;

    public function __construct(float $x, float $y) {
        $this->x = $x;
        $this->y = $y;
    }

    public function getX(): float { return $this->x; }
    public function getY(): float { return $this->y; }

    // Отсутствие сеттеров (setX, setY) предотвращает изменение.
    public function translate(float $dx, float $dy): self {
        return new self($this->x + $dx, $this->y + $dy);
    }
}

3. Важное замечание про объекты внутри свойств: Если свойство является объектом, даже readonly защищает только ссылку, но не внутреннее состояние объекта.

class ImmutableContainer {
    public function __construct(
        public readonly array $items, // Массив можно заменить, но не изменить? Внимание!
        public readonly DateTime $date // Объект DateTime мутабелен!
    ) {}
}

$container = new ImmutableContainer([1, 2], new DateTime());
// $container->items = [3, 4]; // Ошибка: нельзя переназначить свойство.
$container->items[] = 3; // Ошибка? НЕТ! Это изменит массив (если он не был frozen).
$container->date->modify('+1 day'); // Ошибка? НЕТ! Изменяет внутреннее состояние объекта.

Решение: Для полной иммутабельности нужно использовать иммутабельные объекты (например, DateTimeImmutable вместо DateTime), возвращать копии массивов или использовать коллекции, которые не позволяют модификацию.