Клонирование readonly свойств в PHP 8.3
В идеальном мире мы могли бы клонировать классы с readonly
свойствами, на основе определённого пользователем набора значений. Так называемый синтаксис clone with
(которого не существует):
readonly class Post
{
public function __construct(
public string $title,
public string $author,
public DateTime $createdAt,
) {}
}
$post = new Post(
title: 'Hello World',
// …
);
// Это невозможно!
$updatedPost = clone $post with {
title: 'Another One!',
};
Читая заголовок текущего RFC:
— вы могли подумать, что теперь возможно что-то вроде Readonly
свойства могут быть повторно инициализированы во время клонированияclone with
. Однако… это не так. RFC разрешает только одну конкретную операцию: перезаписывать readonly
значения в магическом методе __clone
:
readonly class Post
{
public function __construct(
public string $title,
public string $author,
public DateTime $createdAt,
) {}
public function __clone()
{
$this->createdAt = new DateTime();
// Это разрешено,
// несмотря на то, что `createdAt` является readonly свойством
}
}
Это полезно? Да! Скажем, вы хотите клонировать объекты с вложенными объектами — т.е. создание глубоких клонов
; затем этот RFC позволяет вам клонировать и эти вложенные объекты и перезаписывать их во вновь созданном клоне, даже если они являются readonly
свойствами.
readonly class Post
{
public function __clone()
{
$this->createdAt = clone $this->createdAt;
// Создаёт новый объект DateTime,
// вместо повторного использования ссылки
}
}
Без этого RFC вы могли бы клонировать $post
, но он по-прежнему содержал бы ссылку на исходный объект $createdAt
. Скажем, вы вносите изменения в этот объект (что возможно, поскольку readonly
только предотвращает изменение назначенного свойства, а не изменение его внутренних значений):
$post = new Post(/* … */);
$otherPost = clone $post;
$post->createdAt->add(new DateInterval('P1D'));
$otherPost->createdAt === $post->createdAt; // true :(
Тогда вы получите изменение даты $createdAt
для обоих объектов!
Благодаря этому RFC мы можем создавать настоящие клоны со всеми вложенными свойствами, даже если это readonly
свойства:
$post = new Post(/* … */);
$otherPost = clone $post;
$post->createdAt->add(new DateInterval('P1D'));
$otherPost->createdAt === $post->createdAt; // false :)
От себя, лично
Я думаю, хорошо, что PHP 8.3 делает возможным глубокое клонирование readonly
свойств. Однако у меня смешанные чувства по поводу этой реализации. Представьте на секунду, что clone with
существовало в PHP, тогда всё вышеперечисленное было бы ненужным. Взглянем:
// Опять, это не возможно!
$updatedPost = clone $post with {
createdAt: clone $post->createdAt,
};
А теперь представьте, что clone with
добавляется в PHP 8.4 — чистая спекуляция, конечно. Это означает, что у нас будет два способа сделать одно и то же в PHP. Не знаю, как вам, а мне не нравится, когда языки или фреймворки предлагают несколько способов сделать одно и то же. Насколько я понимаю, это в лучшем случае не оптимальный дизайн языка.
Это, конечно, при условии, что clone with
сможет автоматически сопоставлять значения со свойствами без необходимости вручную реализовывать логику сопоставления в __clone
. Я также предполагаю, что clone with
может иметь дело с видимостью свойств: он может изменять только публичные свойства извне, но может изменять защищённые и приватные при использовании внутри класса.
Некоторое время назад я писал, что внутри PHP кажется разделённым: одна группа предлагает одно решение, а другая группа хочет использовать другой подход. На мой взгляд, это явный недостаток разработки комитетом.
Полное раскрытие информации — RFC упоминает clone with
в качестве будущей области применения:
Ни одна из предполагаемых идей на будущее не противоречит предложениям в этом RFC. Таким образом, их можно было бы рассматривать отдельно позже.
Но я склонен не согласиться с этим утверждением, по крайней мере, предполагая, что clone with
будет работать без необходимости реализации какого-либо пользовательского кода. Если бы следовали тенденциям текущего RFC, я мог бы представить, что кто-то предлагает добавить clone with
только как способ передачи данных в __clone
, и пользователи сами справятся с этим:
readonly class Post
{
public function __clone(...$properties)
{
foreach ($properties as $name => $value) {
$this->$name = $value;
}
}
}
Тем не менее я действительно надеюсь, что это не тот способ, которым реализуется clone with
; потому что вам придётся добавить реализацию __clone
для каждого readonly
класса.
Итак, если предположить, что в лучшем случае добавляется clone with
, а когда он сможет автоматически сопоставлять значения; тогда функциональность этого текущего RFC аннулируется, и у нас есть два способа сделать одно и тоже. Это смутит пользователей, потому что ставит перед ними ещё одно решение при кодировании. Я думаю, что PHP и так стал достаточно запутанным, и я хотел бы увидеть как это изменится.
С другой стороны, я хочу отметить, что не выступаю против этого RFC сам по себе. Я думаю, что Nicolas и Máté отлично поработали, найдя надёжное решение реальной проблемы.
P.S: на случай, если кто-то хочет привести аргумент в пользу текущего RFC, потому что вам нужно реализовать __clone
только один раз для каждого объекта и больше не беспокоиться об этом. В этих изолированных примерах отсутствует одна очень важная деталь: глубокое копирование не происходит с помощью простого вызова clone
. В большинстве случаев используются такие пакеты, как deep-copy, и, таким образом, потенциальные накладные расходы, связанные с моим примером clone with
, уже устранены этими пакетами и не беспокоят конечных пользователей.