Глава 49 · От основ к архитектуре
Клонирование: копия заказа, вложенные объекты и PHP 8.5
Различаем присваивание, поверхностную копию, выборочное глубокое копирование и новый бизнес-объект. Проверяем циклы и readonly.
Сделать второй черновик карточки и не изменить первый
Редактор копирует карточку чая, чтобы подготовить новое название. Присваивание объекта второй переменной не создаёт независимую карточку. Оператор clone создаёт новый объект с поверхностно скопированными свойствами.
Что получится. Исходная карточка останется Чай, копия станет Чёрный чай.
- $draft — исходная карточка
- clone создаёт отдельный экземпляр в $copy
- Строковое поле title копии меняется
- Оригинальная строка title остаётся прежней
<?php
declare(strict_types=1);
class ProductCard { public string $title = 'Чай'; }
$draft = new ProductCard();
$copy = clone $draft;
$copy->title = 'Чёрный чай';
echo 'Оригинал: ', $draft->title, "\n";
echo 'Копия: ', $copy->title, "\n";
Ожидаемый результат
Оригинал: Чай
Копия: Чёрный чай
Слева новая переменная, справа clone исходного объекта. Если убрать слово clone, обе переменные дадут доступ к одной карточке, и изменение станет видно через обе.
| Запись в примере | Что делает каждый элемент |
|---|---|
$copy = $draft; | Второе имя доступа к тому же объекту. |
$copy = clone $draft; | Другой объект с копией свойств; вложенные объекты пока остаются общими. |
__clone(): void | Дополнительный метод выполняется на новой копии после обычного шага копирования. |
Повтор заказа — отдельное бизнес-действие
Два черновика ещё могут разделять один адрес
При повторе заказа меняем город в копии. Нужно заранее решить, должен ли исходный адрес остаться прежним.
- Обычный clone: копия снаружи
Draft уже два, но вложенный Address ещё один на оба черновика.
- Исходный Draft → общий Address
Сначала city = Москва. Свойство содержит тот же вложенный объект, что и копия.
- Копия Draft → общий Address
Правка города через копию меняет общий Address на Казань.
- Чтение из исходного: Казань
Внешние объекты различны, но вложенное состояние разделялось.
- __clone: выбранные вложения копируются
Метод явно клонирует Address и каждую Line, а ID копии сбрасывает в null.
- Исходный Draft → свой Address
Москва и количество 2 сохраняются после изменения копии.
- Копия Draft → отдельный Address
Казань и количество 5 принадлежат новому черновику.
- ID копии: null
Старый номер не выдаётся за новую сущность; сохранение и выдача ID — отдельный шаг.
Это два самостоятельных опыта главы. PHP не выполняет универсальное глубокое копирование графа автоматически. Выбранная граница копии должна учитывать циклы и вложения, которые намеренно остаются общими.
clone не знает, что новый заказ должен получить новый номер, неоплаченный статус и актуальную доступность товара. Для кнопки «Повторить заказ» часто нужна фабрика нового заказа из выбранных данных старого. Копирование полезно для независимого черновика или снимка, когда точно определена граница владения.
| Данные исходного заказа | Поведение нового заказа |
|---|---|
| Адрес-снимок | Копировать, если его разрешено редактировать независимо |
| Номер заказа и операция оплаты | Создать новые по бизнес-правилам |
| Неизменяемое значение цены | Можно разделить объект, если договор действительно неизменяем |
| HTTP-клиент или журнал | Обычно использовать готовую зависимость, не клонировать |
Вложенный адрес остался общим
<?php
declare(strict_types=1);
class Address { public string $city = 'Тула'; }
class Draft
{
public function __construct(public Address $address) {}
}
$original = new Draft(new Address());
$copy = clone $original;
$copy->address->city = 'Казань';
echo 'Оригинал: ', $original->address->city, "\n";
echo 'Копия: ', $copy->address->city, "\n";
Ожидаемый результат
Оригинал: Казань
Копия: Казань
Draft уже два, Address пока один. Копирование свойства сохранило доступ к тому же вложенному объекту. Такая ошибка — реальная причина вопросов о копировании объектов; следующий шаг разрывает именно эту связь.
Копировать принадлежащий черновику адрес в __clone
<?php
declare(strict_types=1);
class Address { public string $city = 'Тула'; }
class Draft
{
public function __construct(public Address $address) {}
public function __clone(): void { $this->address = clone $this->address; }
}
$original = new Draft(new Address());
$copy = clone $original;
$copy->address->city = 'Казань';
echo 'Оригинал: ', $original->address->city, "\n";
echo 'Копия: ', $copy->address->city, "\n";
Ожидаемый результат
Оригинал: Тула
Копия: Казань
В __clone переменная $this — новая копия Draft. Она получает свою копию Address. Метод не заменяет встроенное клонирование и не вызывает конструктор заново. Если у родителя есть нужная очистка в __clone, собственный метод потомка должен вызвать её явно.
- original → первый Draft → Address(Тула)
- clone создаёт второй Draft
- __clone заменяет адрес второго Draft отдельным Address
- Запись Казань затрагивает адрес только второго Draft
Копия с изменённым readonly-полем — PHP 8.5+
<?php
declare(strict_types=1);
final class Offer
{
public function __construct(public readonly int $priceKopecks)
{
if ($priceKopecks < 0) { throw new InvalidArgumentException('Отрицательная цена'); }
}
public function withPrice(int $priceKopecks): self
{
if ($priceKopecks < 0) { throw new InvalidArgumentException('Отрицательная цена'); }
return clone($this, ['priceKopecks' => $priceKopecks]);
}
}
$original = new Offer(25900);
$discounted = $original->withPrice(23900);
echo 'Было: ', $original->priceKopecks, " коп.\n";
echo 'Предложение: ', $discounted->priceKopecks, " коп.\n";
Ожидаемый результат
Было: 25900 коп.
Предложение: 23900 коп.
PHP 8.5: второй аргумент clone — массив заменяемых свойств. Замены применяются после __clone и соблюдают видимость. Конструктор заново не запускается, поэтому withPrice повторяет проверку входа. Для PHP 8.2–8.4 здесь достаточно return new self($priceKopecks), что также повторно использует конструктор.
Владение решает, какую часть графа копировать
Снимок адреса принадлежит заказу, а клиент API обслуживает много заказов. Поэтому адрес копируют, клиента обычно разделяют. Массив объектов тоже содержит обращения к объектам: копия массива не превращает каждый элемент в новый экземпляр. Для повторного заказа лучше явно выбрать строки, адрес и новый статус, чем надеяться на универсальный clone.
Попробуйте. Добавьте адресу street = "Садовая" и измените только улицу копии после исправленного __clone.
Разобрать решение
Оба скалярных поля Address скопируются автоматически при clone адреса. У оригинала останется Садовая. Если street сам станет объектом, потребуется новое решение о его владении.
Циклы, ссылки, ресурсы и стоимость
Граф может возвращаться к уже посещённому объекту: заказ → клиент → заказы. Наивное рекурсивное клонирование каждого поля тогда не завершится; нужен учёт уже скопированных объектов и сохранение намеренно общих связей. PHP-ссылки & в свойствах тоже сохраняют свою связь при поверхностной копии. Некоторые внутренние объекты и ресурсы вообще нельзя осмысленно клонировать.
Не используйте serialize/unserialize как универсальное копирование: оно имеет другой договор, ограничения и способно запускать специальные методы. Стоимость глубокой копии зависит от размера достижимого графа; измеряйте её на своём наборе данных. Ссылка на исходную постановку вопроса о копии полезна как обсуждение проблемы, а правила берём из PHP Manual.
Дальше: полный договор языка и специальные случаи
PHP 8.3 отдельно разрешает повторную инициализацию readonly-свойства внутри __clone для построения независимого вложенного состояния. Это правило сохранено ниже и отличается от синтаксиса clone с массивом замен PHP 8.5.
Разделы ниже сохраняют специальные правила официального PHP Manual: Readonly при клонировании: один раз заменить вложенный объект — PHP 8.3+.
Readonly при клонировании: один раз заменить вложенный объект — PHP 8.3+
У снимка настройки ссылка на вложенный объект readonly, но сам вложенный объект изменяем. Обычный shallow clone разделит его с оригиналом. Начиная с PHP 8.3, __clone может повторно инициализировать readonly-свойство клона: удобно заменить вложенное значение собственной копией.
Это продолжение поверхностного клонирования. Разрешение действует во время клонирования для нового экземпляра; оно не открывает обычную последующую запись в readonly.
- Оригинал содержит readonly-ссылку на csv-настройки
- clone сначала копирует ссылку
- __clone заменяет ссылку новым clone вложенного объекта
- Изменение вложенного клона не затрагивает оригинал
<?php
declare(strict_types=1);
class MutableExportOptions
{
public string $format = 'csv';
}
class ExportSnapshot
{
public function __construct(public readonly MutableExportOptions $options) {}
public function __clone()
{
$this->options = clone $this->options;
}
}
$original = new ExportSnapshot(new MutableExportOptions());
$copy = clone $original;
$copy->options->format = 'json';
echo $original->options->format, ', ', $copy->options->format, "\n";
echo $original->options === $copy->options ? "Общая настройка\n" : "Настройки разделены\n";
try {
$copy->options = new MutableExportOptions();
} catch (Error $error) {
echo "Повторная обычная запись запрещена\n";
}
Ожидаемый результат
csv, json
Настройки разделены
Повторная обычная запись запрещена
В __clone переинициализировано свойство нового ExportSnapshot. Значение оригинала не переприсваивалось. Вложенный format всё ещё изменяем, потому что readonly относится к внешнему свойству options, а не ко всему графу объектов.
Даже внутри __clone это не неограниченная изменяемость: повторная инициализация поля разрешается один раз. В PHP 8.4 запрещена и косвенная модификация readonly при клонировании через получение ссылки; используйте прямое присваивание проверенного значения. Для __clone можно явно указать возвращаемый тип void; новое скопированное значение возвращает сама операция clone.
Если вложенное значение действительно неизменяемо, разделять его может быть корректно и дешевле, чем копировать. Для соединения с БД, транспорта или логгера клонирование обычно вообще не означает нового независимого ресурса. Правило копирования выбирают по владению данными, а не по количеству readonly в объявлении.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
cloneСоздать новый верхний экземпляр на основе существующего.
clone $object; // PHP8.5+: clone($object, array $withProperties = [])Оператор делает поверхностную копию, затем вызывает доступный __clone на копии. Возвращает новый объект. Некоторые внутренние объекты не поддерживают клонирование и вызывают Error.
<?php
declare(strict_types=1);
$delivery = new DateTime('2026-10-12', new DateTimeZone('UTC'));
$rescheduled = clone $delivery;
$rescheduled->modify('+2 days');
echo $delivery->format('Y-m-d'), '/', $rescheduled->format('Y-m-d'), "\n";
Ожидаемый результат
2026-10-12/2026-10-14
Для нового кода DateTimeImmutable часто убирает необходимость в явном clone.
<?php
declare(strict_types=1);
final class Filter { public string $category = 'tea'; public int $page = 1; }
$base = new Filter();
$next = clone $base;
$next->page = 2;
echo $base->page, '/', $next->page, "\n";
Ожидаемый результат
1/2
Все поля скалярные, поэтому поверхностной копии здесь достаточно.
Рядом: __clone, spl_object_id.
__cloneНастроить уже созданную копию и разорвать выбранные связи.
public function __clone(): voidВызывается на новом объекте после поверхностного копирования. Подходит для сброса идентификатора черновика и копирования конкретных изменяемых вложенных объектов.
<?php
declare(strict_types=1);
final class Address { public string $city = 'Москва'; }
final class Draft {
public function __construct(public Address $address) {}
public function __clone(): void { $this->address = clone $this->address; }
}
$a = new Draft(new Address()); $b = clone $a; $b->address->city = 'Тула';
echo $a->address->city, '/', $b->address->city, "\n";
Ожидаемый результат
Москва/Тула
Явно копируется только адрес, за который отвечает черновик.
<?php
declare(strict_types=1);
final class Template {
public function __construct(public ?int $id, public string $title) {}
public function __clone(): void { $this->id = null; $this->title .= ' (копия)'; }
}
$copy = clone new Template(8, 'Письмо');
echo ($copy->id === null ? 'new:' : 'old:'), $copy->title, "\n";
Ожидаемый результат
new:Письмо (копия)
Сохранение в БД выдаст новый ID; само клонирование ничего не записывает.
Источники и обсуждения
- Клонирование
- Readonly при клонировании
- Readonly clone в 8.3
- Clone with в 8.5
- Границы unserialize
- Глубокое копирование — обсуждение проблемы
- Присваивание объекта не создаёт независимую копию — обсуждение проблемы