PHP 8.1: Клонирование и изменение readonly-свойств

Источник: «PHP 8.1: cloning and changing readonly properties»
В PHP 8.1 введены readonly-свойства. Они избавляют от множества проблем, но и добавляют новую — запрет на переопределение readonly-свойств клонированного объекта. Давайте разберёмся с решением этой проблемы и найдём обходной путь.

В PHP 8.1, readonly-свойства нельзя переопределять после инициализации. Это также означает, что клонирование объекта и изменение одного из его readonly-свойств не разрешено. Вполне вероятно, что в будущем PHP получит что-то вроде clone with, но пока нам придётся обойти эту проблему.

Давайте представим простой класс DTO с readonly-свойствами:

class Post
{
public function __construct(
public readonly string $title,
public readonly string $author,
) {}
}

PHP 8.1 выдаёт ошибку когда вы клонируете объект Post и пытаетесь переопределить одно из его свойств доступных только для чтения:

$postA = new Post(title: 'a', author: 'Brent');

$postB = clone $postA;
$postB->title = 'b';
Error: Cannot modify readonly property Post::$title

Причина по которой это происходит, заключается в том, что текущая реализация readonly-свойств позволяет устанавливать значение только до тех пока они не инициализировано. Поскольку мы клонируем объект, свойствам которого уже присвоено значение, мы не можем его переопределить.

Весьма вероятно, что в будущем в PHP добавят какой-то механизм для клонирования объектов и переопределения свойств доступных только для чтения. Но с приближающейся заморозкой добавления нового функционала в PHP 8.1 мы можем быть уверены, что на данный момент добавление этого функционала не произойдёт.

Итак, по крайней мере для PHP 8.1 у нас есть способ обойти эту проблему. Именно это я и сделал, и поэтому я создал пакет, который вы можете использовать spatie/php-cloneable.

Вот как это работает. Сначала вы загружаете пакет с помощью composer, а затем используете трейт Spatie\Cloneable\Cloneable во всех классах, которые хотите клонировать:

use Spatie\Cloneable\Cloneable;

class Post
{
use Cloneable;

public function __construct(
public readonly string $title,
public readonly string $author
) {}
}

Теперь у нашего объекта Post будет метод with, который вы можете использовать для копирования и переопределения свойств:

$postA = new Post(title: 'a', author: 'Brent');

$postB = $postA->with(title: 'b');
$postC = $postA->with(title: 'c', author: 'Freek');

Конечно есть несколько предостережений:

Я полагаю, что этот пакет полезен для простых (DTO) и (VO), являющихся именно теми типами объектов, для которых изначально были разработаны readonly-свойства.

Для моих случаев использования этой реализации достаточно. И поскольку я верю в дизайн основанный на мнении (opinion-driven design), я так же не заинтересован в добавлении к нему дополнительного функционала: этот пакет решает одну конкретную проблему, и этого достаточно.

Дополнительные материалы

Предыдущая Статья

PHP 8.1: Readonly-свойства / свойства только для чтения

Следующая Статья

PHP 8.2: Readonly-классы / классы только для чтения