Александр Некрасов · Автономное руководство

PHP: полный мануал с примерами, функциями и ООП

Разделы этой страницы
  1. Клонирование: копия заказа, вложенные объекты и PHP 8.5
  2. Сделать второй черновик карточки и не изменить первый
  3. Повтор заказа — отдельное бизнес-действие
  4. Вложенный адрес остался общим
  5. Копировать принадлежащий черновику адрес в __clone
  6. Копия с изменённым readonly-полем — PHP 8.5+
  7. Владение решает, какую часть графа копировать
  8. Циклы, ссылки, ресурсы и стоимость
  9. Дальше: полный договор языка и специальные случаи
  10. Readonly при клонировании: один раз заменить вложенный объект — PHP 8.3+
  11. Функции и методы этой темы
  12. Код и руководство с собой
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 49 · От основ к архитектуре

Клонирование: копия заказа, вложенные объекты и PHP 8.5

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

Сделать второй черновик карточки и не изменить первый

Редактор копирует карточку чая, чтобы подготовить новое название. Присваивание объекта второй переменной не создаёт независимую карточку. Оператор clone создаёт новый объект с поверхностно скопированными свойствами.

Что получится. Исходная карточка останется Чай, копия станет Чёрный чай.

Как проходит конкретный вызов
  1. $draft — исходная карточка
  2. clone создаёт отдельный экземпляр в $copy
  3. Строковое поле title копии меняется
  4. Оригинальная строка 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, собственный метод потомка должен вызвать её явно.

Связи после исправления
  1. original → первый Draft → Address(Тула)
  2. clone создаёт второй Draft
  3. __clone заменяет адрес второго Draft отдельным Address
  4. Запись Казань затрагивает адрес только второго 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.

Клон получает собственные вложенные настройки
  1. Оригинал содержит readonly-ссылку на csv-настройки
  2. clone сначала копирует ссылку
  3. __clone заменяет ссылку новым clone вложенного объекта
  4. Изменение вложенного клона не затрагивает оригинал
Отделить вложенное значение, сохранив readonly-свойство — PHP 8.3+Самостоятельный пример
<?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Создать новый верхний экземпляр на основе существующего.

PHP 8.2+ · Core

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Настроить уже созданную копию и разорвать выбранные связи.

PHP 8.2+ · Core

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";
Ожидаемый результат
Москва/Тула

Явно копируется только адрес, за который отвечает черновик.

Сбросить ID при дублировании шаблонаСамостоятельный пример
<?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; само клонирование ничего не записывает.

Рядом: clone, WeakMap.

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.