PHP 8.1: Клонирование и изменение 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');
Конечно есть несколько предостережений:
- Этот пакет будет пропускать вызов конструктора при клонировании объекта. Это означает, что никакая логика в конструкторе не будет выполняться.
- Метод
with
выполняет не глубокое копирование (shallow copy), а значит вложенные объекты не клонируются.
Я полагаю, что этот пакет полезен для простых (DTO) и (VO), являющихся именно теми типами объектов, для которых изначально были разработаны readonly-свойства.
Для моих случаев использования этой реализации достаточно. И поскольку я верю в дизайн основанный на мнении (opinion-driven design), я так же не заинтересован в добавлении к нему дополнительного функционала: этот пакет решает одну конкретную проблему, и этого достаточно.