Перейти к содержимому
Поиск
Php Разработка

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

Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 18 · После функций и массивов

Ссылки: когда два имени изменяют одно значение

От двух имён одного значения до передачи и возврата ссылок, перепривязки, unset, общей структуры массивов и диагностики ReflectionReference.

Два ярлыка у одного значения: что такое ссылка PHP

В учебном примере остаток называют stock, а в другом месте — available. Иногда нужно, чтобы изменение через любое имя было видно через другое. Ссылка PHP связывает имена с общим значением. Аналогия — два ярлыка на одном ящике: переименованием ярлыка не создаётся второй ящик. Это модель поведения переменных, а не схема физических адресов памяти.

Один остаток, два имени
  1. $stock = 12; — создаём значение
  2. $available =& $stock; — связываем второе имя
  3. $available = 10; — изменяем общее значение
  4. echo $stock; — видим 10 через первое имя

Знак $ начинает имя переменной. Обычный = присваивает значение. В записи =& амперсанд задаёт присваивание по ссылке; пробел между = и & допустим. Echo выводит значение; точка с запятой заканчивает инструкцию.

Изменить остаток через второе имяСамостоятельный пример
<?php
$stock = 12;
$available =& $stock;
$available = 10;
echo $stock;
Ожидаемый результат
10

available не хранит независимую копию числа: изменение видно через stock. Здесь мы изучаем механизм; для складской системы с одновременными заказами одних переменных в памяти процесса недостаточно.

Сравнить с обычным присваиваниемСамостоятельный пример
<?php
$stock = 12;
$available = $stock;
$available = 10;
echo $stock;
Ожидаемый результат
12

Без & изменение второго числа не меняет первое. PHP может использовать внутренние оптимизации памяти, но они не отменяют это наблюдаемое различие. Не добавляйте & просто ради предположения об экономии памяти.

Изменить значение и перепривязать имя — разные действия

Обычное присваивание через любое имя меняет общее значение. Новое присваивание по ссылке перепривязывает только имя слева. Оно не перемещает весь прежний набор связанных имён к новому значению. PHP-ссылка не является указателем C: у неё нет доступной арифметики адресов.

Перевести один ярлык к другому остаткуСамостоятельный пример
<?php
$firstStock = 12;
$secondStock = 20;
$current =& $firstStock;
$current =& $secondStock;
$current = 21;
echo $firstStock, '/', $secondStock;
Ожидаемый результат
12/21

current теперь связан со secondStock; firstStock остался 12. Символ / в выводе — обычный разделитель текста. Поэтому функция, перепривязавшая свой локальный параметр, не обязательно перепривяжет переменную вызывающего кода.

Обычное чтение из ссылки даёт значениеСамостоятельный пример
<?php
$count = 3;
$alias =& $count;
$snapshot = $alias;
$alias = 4;
echo $count, '/', $snapshot;
Ожидаемый результат
4/3

snapshot получил обычное число, а не ещё один ярлык. Чтобы присоединить третий ярлык, потребовалось бы $snapshot =& $alias. У массивов с внутренними ссылками и объектов есть дополнительные правила, которые ниже разобраны отдельно.

unset снимает имя, присваивание null меняет значение

unset($alias) убирает привязку этого имени. Другие имена сохраняют своё значение. Присваивание $alias = null оставляет связь, но записывает null в общее значение. Эти действия решают разные задачи.

Убрать один ярлык и использовать имя зановоСамостоятельный пример
<?php
$count = 3;
$alias =& $count;
unset($alias);
$alias = 9;
echo $count, '/', $alias;
Ожидаемый результат
3/9

После unset новое присваивание создаёт отдельную переменную. Так разрывают временную связь после foreach по ссылке. Это не удаление товара из массива: для него нужно unset($items[$key]).

Изменить общее значение на nullСамостоятельный пример
<?php
$count = 3;
$alias =& $count;
$alias = null;
var_dump($count);
Ожидаемый результат
NULL

var_dump показывает тип и значение. Оба имени всё ещё связаны; null видно через count. isset не отличает такую переменную от отсутствующей: он проверяет наличие ненулевого значения.

Ссылка на отсутствующий элемент может сама создать его

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

Заметить, что чтение по ссылке создало полеСамостоятельный пример
<?php
$order = [];
$comment =& $order['comment'];
if (array_key_exists('comment', $order)) {
    echo 'Поле уже существует: ', $order['comment'] ?? 'ещё не заполнено', "\n";
}
$comment = 'Позвонить заранее';
echo 'Памятка сборщику: ', $order['comment'], "\n";
Ожидаемый результат
Поле уже существует: ещё не заполнено
Памятка сборщику: Позвонить заранее

Связь с отсутствующим comment создала этот ключ со значением null. Array_key_exists возвращает true и разрешает первую подпись; ?? подставляет текст для null. Запись через comment затем меняет поле самого order, откуда берётся памятка. Обычное чтение order[comment] ?? null не создало бы ключ. Поэтому параметр по ссылке нельзя бездумно добавлять к функции, которая должна лишь проверить наличие данных.

Проверьте себя. Можно ли обернуть чтение в function read(&$value), чтобы просто посмотреть отсутствующий ключ?

Разбор

Такой параметр передаётся по ссылке и может создать ключ. Если функция должна только читать, принимайте значение без & и отдельно определите поведение при отсутствии.

Почему функция может изменить вашу переменную

После вызова функции остаток на складе неожиданно изменился, хотя вы ничего не присваивали $stock. Один из возможных ответов — параметр принят по ссылке. Такая функция намеренно работает с переменной вызывающего кода. Прежде чем применять это к программе, увидим связь на двух именах.

Результат: вы прочитаете запись с &, поймёте её побочный эффект и сможете убрать случайную связь после foreach. Нужны переменные, параметры функций и массивы.

Каждый пример запускается отдельно. Скачайте его архив, распакуйте целиком, откройте терминал в этом каталоге и выполните php example.php. Сравните вывод с блоком «Ожидаемый результат». Не объединяйте примеры в один файл: некоторые намеренно повторяют имена функций и классов.

Три места, где здесь встречается &

$available =& $stock;
function reserveUnits(int &$available, int $quantity): void { /* ... */ }
foreach ($items as &$value) { /* ... */ }
unset($value);
ЗаписьРазбор
$available =& $stockИмена available и stock обращаются к одному значению. Это не копия текущего числа.
int &$availableПараметр ожидает переменную типа int и может изменить её у вызывающего кода.
as &$valueНа каждой итерации value связывается с текущим элементом массива.
unset($value)Удалить это имя/связь. Остальные имена и соответствующий элемент массива сохраняются.

Ссылка PHP — связь имён, а не адрес памяти с арифметикой указателей. Синтаксис & также бывает оператором побитового И между числами; смысл определяется местом записи. Здесь нас интересует именно связывание переменных.

Минимальный опыт: 12 стало 10

Наглядная модель

Два имени обращаются к одному значению

После $available =& $stock меняем доступное количество через available. Через stock видим то же изменение.

  • Общее значение: 12 → 10

    Один остаток уменьшили на две единицы.

    • $stock

      Чтение этого имени теперь даёт 10.

    • $available

      Второе имя связано с тем же значением через =&.

unset($available) снимает одно имя. Значение $stock остаётся 10; это не удаление данных склада.

Два имени одного остатка, затем разрыв связиСамостоятельный пример
<?php
declare(strict_types=1);

$stock = 12;
$available =& $stock;
$available -= 2;
echo 'Остаток: ', $stock, "\n";

unset($available);
$available = 99;
echo 'Старый остаток: ', $stock, "\n";
echo 'Новая переменная: ', $available, "\n";
Ожидаемый результат
Остаток: 10
Старый остаток: 10
Новая переменная: 99

=& связывает два имени с одним значением. Изменение через available поэтому видно через stock. unset убирает имя available из этой связи, а не обнуляет stock. Последующее присваивание создаёт отдельную переменную. Это демонстрация механики, не рекомендованная архитектура складского учёта.

Параметр по ссылке: право изменить переменную вызывающего кода

Обычный параметр получает значение. Запись function markSent(&$status) создаёт параметр, связанный с переданной переменной. В самом вызове & не пишут. Фигурные скобки ограничивают тело функции; изменение параметра видно после возврата. Сначала — один эффект без типизации и дополнительных проверок.

Поменять состояние одной переменнойСамостоятельный пример
<?php
function markSent(&$status)
{
    $status = 'sent';
}
$status = 'new';
markSent($status);
echo $status;
Ожидаемый результат
sent

Вызов не присваивал результат, но status изменился. Такое поведение должно быть очевидно из имени и документации функции. Другой вариант — переписать функцию на возврат нового значения через return и присвоить этот результат снаружи. Не присваивайте результат текущей markSent: она не возвращает значение и даст null.

Передают переменную или значение, которое сама функция возвращает по ссылке. Литерал и вычисленное выражение не дают подходящего изменяемого места. Ошибка ниже намеренно перехватывается через try/catch после знакомства с исключениями.

Почему нельзя изменить переданное число 3Самостоятельный пример
<?php
function addOne(int &$count): void
{
    $count += 1;
}
try {
    addOne(3);
} catch (Error $error) {
    echo "Нужна переменная\n";
}
Ожидаемый результат
Нужна переменная

PHP не может связать параметр с литералом. Создайте $count = 3, затем вызовите addOne($count). Старый синтаксис вызова addOne(&$count) в современных версиях не применяется.

return и изменение по ссылке — два канала результата

У ряда функций значение возвращается через return, а дополнительный результат — через параметр с &. Например, сортировка изменяет массив и отдельно сообщает об успехе. Не присваивайте такой признак обратно вместо данных. В следующем примере sort сортирует изменяемый массив.

Прочитать изменённые массы и статус отдельноСамостоятельный пример
<?php
$weights = [300, 100, 200];
$status = sort($weights, SORT_NUMERIC);
if ($status) { echo "Массы упорядочены\n"; }
foreach ($weights as $position => $grams) {
    echo 'Образец ', $position + 1, ': ', $grams, ' г', "\n";
}
Ожидаемый результат
Массы упорядочены
Образец 1: 100 г
Образец 2: 200 г
Образец 3: 300 г

Sort изменяет сам weights через ссылочный первый параметр и возвращает true в status. Проверка статуса разрешает подпись; foreach читает готовые массы из weights. Прежние ключи заменены 0/1/2, поэтому здесь это номера образцов в новом порядке, не постоянные ID. Присваивание weights = sort(weights) подменило бы массив значением true.

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

Ссылка в контракте функции

Функция явно изменяет переданный остатокСамостоятельный пример
<?php
declare(strict_types=1);

function reserveUnits(int &$available, int $quantity): void
{
    if ($quantity < 1 || $quantity > $available) {
        throw new InvalidArgumentException('Недостаточный остаток или неверное количество');
    }
    $available -= $quantity;
}

$stock = 5;
reserveUnits($stock, 2);
echo 'После резерва: ', $stock, "\n";
try {
    reserveUnits($stock, 20);
} catch (InvalidArgumentException) {
    echo "Резерв отклонён\n";
}
echo 'После отказа: ', $stock, "\n";
Ожидаемый результат
После резерва: 3
Резерв отклонён
После отказа: 3

Знак & стоит в объявлении параметра. Вызов reserveUnits($stock, 2) передаёт переменную без дополнительного &: функция получает право изменить именно её. Проверка выполнена до изменения, поэтому отказ оставляет 3. Это память одного процесса; параллельные заказы требуют отдельной транзакции/протокола резервирования в хранилище.

Такой вызов меняет данные снаружи, поэтому имя reserveUnits и документация должны обещать этот эффект. Часто проще вернуть новый остаток: $stock = remainingStock($stock, 2). Присваивание тогда видно в месте вызова. Нельзя передавать литерал 5 или вычисленное выражение туда, где нужен изменяемый параметр. Не добавляйте & ради предположения о скорости: сначала выберите понятный договор и измерьте реальную задачу.

Перепривязка локального параметра не заменяет внешнее имя

Параметр — отдельное имя, которое при передаче по ссылке связывается с внешним значением. Если потом связать сам параметр с другой переменной, перепривязывается это локальное имя. Это показывает, почему PHP-ссылки нельзя объяснять как двойные указатели C.

Локальное имя перешло к другому значениюСамостоятельный пример
<?php
function chooseOther(&$current, &$other): void
{
    $current =& $other;
    $current = 9;
}
$first = 3;
$second = 7;
chooseOther($first, $second);
echo $first, '/', $second;
Ожидаемый результат
3/9

first осталось 3, second изменилось на 9. Если требуется заменить значение first значением second, внутри функции нужно обычное $current = $other, без перепривязки.

unset параметра не удаляет переменную вызывающего кодаСамостоятельный пример
<?php
function forgetLocal(&$value): void
{
    unset($value);
    $value = 'local';
}
$status = 'new';
forgetLocal($status);
echo $status;
Ожидаемый результат
new

Удалено локальное имя value. Новое присваивание относится к новой локальной переменной. Для удаления поля передайте сам массив и нужный ключ, затем выполните unset($array[$key]) с понятным контрактом.

Возврат ссылки: выбрать существующее место для изменения

Иногда функция ищет нужный элемент сложной структуры, а вызывающий код должен менять именно его. function &name() обещает возврат по ссылке. Получатель пишет $selected =& name();. Это отличается от передачи параметра: знак & нужен и в объявлении результата, и в присваивании получателя. В return возвращается переменная, а не выражение вроде $value + 1.

Как изменение проходит обратно
  1. $editable =& draftNote(); вызывает функцию и просит связь
  2. Функция выбирает свою существующую переменную $note
  3. return $note; возвращает связь в место вызова
  4. $editable = 'review'; меняет выбранное значение
  5. Следующий вызов читает review
Ссылка на сохраняемую заметкуСамостоятельный пример
<?php
function &draftNote(): string
{
    static $note = 'new';
    return $note;
}
$editable =& draftNote();
$editable = 'review';
$copy = draftNote();
$copy = 'local';
echo draftNote(), '/', $copy;
Ожидаемый результат
review/local

static сохраняет note между вызовами в пределах одного выполнения PHP. Обычные HTTP-запросы FPM начинают это состояние заново, даже если процесс повторно используется. У длительного CLI-worker одно выполнение может обрабатывать много заданий. editable связан с note, copy получил обычную строку. Для независимых заказов используйте явное хранилище или объект; этот static не является сохранением заметки между HTTP-запросами.

Теперь практический поиск по артикулу. Массив rows передаётся по ссылке, чтобы поиск выбрал элемент исходной корзины. array_key_exists проверяет наличие; исключение прекращает поиск неизвестного товара. Для новой архитектуры метод updateQuantity с проверкой границ обычно безопаснее, чем выдача прямой ссылки на внутреннюю структуру.

Сначала ниже будет выбрана одна существующая строка корзины. Затем вызывающий код изменит количество через полученную связь и пересчитает весь заказ. Так видно практическое назначение возврата ссылки, а не только символы &.

Изменить выбранную строку и пересчитать корзинуСамостоятельный пример
<?php
function &cartRow(array &$rows, string $sku): array {
    if (!array_key_exists($sku, $rows)) {
        throw new OutOfBoundsException('Нет такого артикула');
    }
    return $rows[$sku];
}
$cart = ['tea' => ['quantity' => 1, 'price' => 150], 'cup' => ['quantity' => 2, 'price' => 200]];
$row =& cartRow($cart, 'tea');
$row['quantity'] = 3;
unset($row);
$total = 0;
foreach ($cart as $sku => $line) {
    $lineTotal = $line['quantity'] * $line['price'];
    $total += $lineTotal;
    echo $sku, ': ', $line['quantity'], ' × ', $line['price'], ' = ', $lineTotal, ' коп.', "\n";
}
echo 'К оплате: ', $total, ' коп.', "\n";
Ожидаемый результат
tea: 3 × 150 = 450 коп.
cup: 2 × 200 = 400 коп.
К оплате: 850 коп.

Первый & у параметра rows связывает его с исходной cart. Амперсанд перед именем функции обещает возврат ссылки на выбранную строку rows[tea]; =& у вызывающего сохраняет эту связь в row. Присваивание quantity=3 меняет строку cart, после чего unset снимает временное имя. Следующий foreach читает уже 3, считает 450+400=850 и показывает итог. Без =& получатель получил бы независимое значение массива. Форма/цены/количества заранее проверены; поиск по ссылке сам не делает валидацию или запись в базу.

Ссылочный результат как изменяемый аргументСамостоятельный пример
<?php
function &attemptCounter(): int
{
    static $count = 0;
    return $count;
}
function recordAttempt(int &$count): void
{
    $count += 1;
}
recordAttempt(attemptCounter());
echo attemptCounter();
Ожидаемый результат
1

Здесь допустим результат вызова: attemptCounter явно возвращает по ссылке. Обычную функцию, возвращающую число по значению, подставлять вместо неё нельзя. В прикладном коде такой скрытый счётчик усложняет тесты; явная переменная или объект обычно понятнее.

Практическая ловушка: ссылка пережила цикл

Поймать последнюю ссылку после foreach и исправитьСамостоятельный пример
<?php
declare(strict_types=1);

$broken = [2, 3, 4];
foreach ($broken as &$value) {
    $value *= 2;
}
// Ошибка: $value всё ещё связан с последним элементом.
foreach ($broken as $value) {
    // Даже пустое тело не отменяет присваивание в $value на каждой итерации.
}
echo 'Без unset: ', json_encode($broken, JSON_THROW_ON_ERROR), "\n";
unset($value);

$fixed = [2, 3, 4];
foreach ($fixed as &$value) {
    $value *= 2;
}
unset($value);
foreach ($fixed as $value) {
}
echo 'С unset: ', json_encode($fixed, JSON_THROW_ON_ERROR), "\n";
Ожидаемый результат
Без unset: [4,6,6]
С unset: [4,6,8]

В первом цикле изменили элементы на месте. После него $value сохранил связь с последним элементом. Второй цикл присваивал этому же имени прочитанные значения и незаметно менял последний элемент. В исправленном варианте связь снята сразу после первого цикла. Альтернатива без ссылок: foreach ($items as $key => $value) { $items[$key] = $value * 2; }.

Блок foreach не создаёт отдельную область видимости переменной. Выполняйте unset сразу после цикла по ссылке, до повторного использования того же имени. Для построения нового списка обычный foreach с добавлением в $result часто лучше показывает, что исходные данные должны сохраниться.

Копия массива может сохранить ссылки внутри элементов

Обычное присваивание массива не означает безусловного глубокого копирования любой структуры. Если элемент уже участвует в общей ссылке, этот общий элемент может сохраниться в копии. Типичная история: взяли короткий alias для вложенного блока API-ответа, затем сохранили «снимок», а редактирование затронуло оба.

Один общий элемент среди обычных данныхСамостоятельный пример
<?php
$order = ['quantity' => 2, 'label' => 'first'];
$quantity =& $order['quantity'];
$snapshot = $order;
$snapshot['quantity'] = 5;
$snapshot['label'] = 'copy';
echo $order['quantity'], '/', $order['label'], "\n";
echo $snapshot['quantity'], '/', $snapshot['label'];
Ожидаемый результат
5/first
5/copy

quantity оказался общим, label остался независимым. Ошибка не случайна и не зависит от похожести имён. Нужно понимать, какие элементы уже были ссылками к моменту копирования.

Убрать временную связь до создания снимкаСамостоятельный пример
<?php
$order = ['quantity' => 2];
$quantity =& $order['quantity'];
$quantity = 3;
unset($quantity);
$snapshot = $order;
$snapshot['quantity'] = 5;
echo $order['quantity'], '/', $snapshot['quantity'];
Ожидаемый результат
3/5

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

Ссылки также могут образовывать самоссылающиеся массивы. Рекурсивный обход или копирование без контроля структуры тогда не завершится корректно. Для внешнего JSON такая ссылка сама не появляется: JSON содержит данные дерева, а связь создал PHP-код. При преобразовании строите новый массив по проверенной схеме, а не обходите произвольный граф без границ.

global и use (&$value): другие места появления связи

global $name внутри функции связывает локальное имя с одноимённой глобальной переменной; это не глобальность всех переменных функции. Перепривязка локального имени подчиняется уже разобранным правилам. Для замыкания use (&$name) создаёт явную связь с переменной внешней области.

Локальный global и последующая перепривязкаСамостоятельный пример
<?php
$mode = 'web';
function localMode(): void
{
    global $mode;
    $temporary = 'cli';
    $mode =& $temporary;
    $mode = 'worker';
}
localMode();
echo $mode;
Ожидаемый результат
web

Обычное $mode = 'worker' сразу после global изменило бы глобальное значение. Но здесь локальное имя сначала переведено к temporary. Это ещё одна причина избегать неочевидных перепривязок в конфигурации.

Счётчик событий у замыканияСамостоятельный пример
<?php
$handled = 0;
$onHandled = function () use (&$handled): void {
    $handled += 1;
};
$onHandled();
$onHandled();
echo $handled;
Ожидаемый результат
2

use перечисляет внешние переменные, & связывает handled с ними. Без & функция получила бы значение при создании замыкания и не меняла внешний handled. Здесь два последовательных локальных вызова; это не потокобезопасный счётчик общей очереди.

Не берите ссылку на весь массив $GLOBALS: современные ограничения не разрешают обращаться с ним как с обычной изменяемой копией глобальной таблицы. Если нужен конкретный глобальный элемент, правила и альтернативы разобраны в суперглобальных массивах.

Почему объекты меняются и без &

Этот дополнительный опыт требует понимания класса и экземпляра. Запись ->city читает свойство, new Address создаёт новый объект. Проверим отдельно изменение свойства и замену переменной.

Общий объект и ссылка на переменную — разные связиСамостоятельный пример
<?php
declare(strict_types=1);

class Address
{
    public function __construct(public string $city) {}
}

$first = new Address('Москва');
$second = $first;
$second->city = 'Казань';
echo 'Общий объект: ', $first->city, "\n";

$second = new Address('Тверь');
echo 'После нового объекта: ', $first->city, '/', $second->city, "\n";

$alias =& $first;
$alias = new Address('Омск');
echo 'После замены через ссылку: ', $first->city, "\n";
unset($alias);
Ожидаемый результат
Общий объект: Казань
После нового объекта: Казань/Тверь
После замены через ссылку: Омск

Обычное $second = $first копирует идентификатор того же объекта: оба имени позволяют изменить его city. Но переназначение second на новый Address не меняет first. С =& связь относится к самой переменной: переназначение alias меняет и то, что хранится под именем first. Для независимой копии объекта изучите clone и вложенные объекты.

Поэтому фраза «все объекты автоматически передаются по ссылке» неточна. Один объект доступен через скопированный идентификатор; ссылка =& — другая связь. Подробный опыт с отдельными копиями находится в главе о клонировании.

Типизированное свойство сохраняет свой контракт через ссылку

Ссылка не отменяет ограничения языка. Обычная переменная может менять тип, но ссылка на типизированное свойство связана с его ограничением. Далее нужен класс и объект: public int задаёт целочисленное свойство, стрелка обращается к нему.

Нельзя обойти int через временное имяСамостоятельный пример
<?php
declare(strict_types=1);
class CartLine
{
    public int $quantity = 2;
}
$line = new CartLine();
$quantity =& $line->quantity;
try {
    $quantity = 'many';
} catch (TypeError $error) {
    echo "Тип отклонён\n";
}
echo $line->quantity;
Ожидаемый результат
Тип отклонён
2

Некорректная запись не изменила свойство. readonly-свойства и свойства с hooks имеют дополнительные ограничения: не считайте, что любое выражение с -> можно безусловно связать через &.

Как находить неочевидное изменение

Что заметилиЧто проверить
Изменилось число после вызоваЗнак & у параметра в сигнатуре функции.
Последний элемент массива внезапно заменилсяОстался ли alias после foreach; есть ли unset сразу после цикла.
Изменились оригинал и копияВложенные ссылки и общий объект; обычное = не делает deep clone.
Переменная исчезла только внутри функцииЛокальный unset или перепривязка параметра.
Текстовые значения одинаковыРавенство значений само по себе не доказывает общую ссылку.

С PHP 7.4 класс ReflectionReference позволяет исследовать ссылку у элемента массива. fromArrayElement возвращает объект описания либо null для обычного элемента; getId даёт непрозрачный идентификатор связи. Это диагностические инструменты после понимания массивов и объектов; не универсальный адрес памяти.

Различить общий элемент и обычное числоСамостоятельный пример
<?php
$quantity = 2;
$data = ['shared' => &$quantity, 'plain' => 2];
$shared = ReflectionReference::fromArrayElement($data, 'shared');
$plain = ReflectionReference::fromArrayElement($data, 'plain');
var_dump($shared !== null);
var_dump($plain === null);
Ожидаемый результат
bool(true)
bool(true)

Оба элемента содержат число 2, но только shared связан с quantity. ===/!== сравнивают также тип. Наличие объекта ReflectionReference само не означает ошибку: ошибка появляется, если общее изменение противоречит вашему контракту.

Проверьте себя. Нужно ли передавать любой большой массив с & для ускорения?

Разбор

Нет. Сначала выберите контракт: изменяет функция исходник или возвращает новый результат. PHP использует copy-on-write; ссылка не является обещанием ускорения и может усложнить изоляцию. Измеряйте реальные данные и окружение, не переносите случайный микротест на весь проект.

Самостоятельная проверка границы изменения

Попробуйте сами. После цикла увеличения [1, 2] на 1 код записал $value = 100. Каким будет массив с unset и без него?

Разбор и проверка

Без снятия ссылки последнее значение станет 100: [2,100]. С unset($value) сразу после foreach массив останется [2,3], а value станет отдельной переменной. Выполните оба варианта в отдельных массивах и сравните вывод json_encode.

Функции и методы этой темы

Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.

ReflectionReference::fromArrayElementУзнать, является ли конкретный элемент массива PHP-ссылкой.

PHP 7.4+ · Reflection

public static ReflectionReference::fromArrayElement(array $array, int|string $key): ?ReflectionReference

Этот инструмент нужен, когда изменение копии массива неожиданно затронуло оригинал. Сначала прочтите ссылки и вложенные общие элементы. ReflectionReference исследует именно PHP-ссылку, а не факт, что два значения указывают на один объект. Класс доступен с PHP 7.4; его объект получают фабричным методом, а не через обычный new.

Параметр / результатКонтракт
array: arrayМассив, существующий элемент которого нужно исследовать. Проверка не создаёт намеренно новую ссылку на этот элемент.
key: int|stringКлюч исследуемого элемента, а не его порядковый номер в foreach.
?ReflectionReferenceОбъект описания для ссылочного элемента, null — для обычного.
Неизвестный ключReflectionException. Это другой случай, чем существующий обычный элемент и null в качестве значения.
Одинаковые числа, разные связиСамостоятельный пример
<?php
$limit = 5;
$options = ['shared' => &$limit, 'plain' => 5];
var_dump(ReflectionReference::fromArrayElement($options, 'shared') !== null);
var_dump(ReflectionReference::fromArrayElement($options, 'plain') === null);
Ожидаемый результат
bool(true)
bool(true)

:: вызывает статический метод класса. Сначала объект описания существует, затем метод вернул null. Равенство двух чисел 5 не доказывает общую связь. Метод также не исследует все вложенные уровни автоматически.

Отличить отсутствующий ключ от обычного элементаСамостоятельный пример
<?php
$options = ['comment' => null];
var_dump(ReflectionReference::fromArrayElement($options, 'comment'));
try {
    ReflectionReference::fromArrayElement($options, 'missing');
} catch (ReflectionException $error) {
    echo "Нет ключа\n";
}
Ожидаемый результат
NULL
Нет ключа

comment существует и не является ссылкой: получен null. missing отсутствует: получено исключение. Если отсутствие допустимо по задаче, предварительно используйте array_key_exists, которое различает эти случаи.

Не оборачивайте произвольную переменную в [&$value] для якобы нейтральной проверки её состояния: этим вы сами создаёте дополнительную связь и меняете условия опыта. Для сравнения двух уже найденных ссылок используйте getId. Для исправления обычно нужно убрать ненужный alias или изменить контракт копирования, а не обходить все данные Reflection на каждом запросе.

ReflectionReference::getIdСравнить идентичность двух уже обнаруженных PHP-ссылок.

PHP 7.4+ · Reflection

public ReflectionReference::getId(): string

Этот инструмент нужен, когда изменение копии массива неожиданно затронуло оригинал. Сначала прочтите ссылки и вложенные общие элементы. ReflectionReference исследует именно PHP-ссылку, а не факт, что два значения указывают на один объект. Класс доступен с PHP 7.4; его объект получают фабричным методом, а не через обычный new.

Часть контрактаСмысл
АргументыМетод не принимает аргументов; вызывается у уже полученного ReflectionReference.
Возвращает stringНепрозрачный идентификатор ссылочной связи. Это бинарная строка, не адрес и не готовый пользовательский текст.
ИдентичностьОбъекты описания одной связи дают один ID; разные живые связи — разные ID. Сравнение выполняйте строго через ===.
Срок использованияДиагностика текущего процесса; не ключ записи в базе и не стабильный ID между запусками.
Два элемента связаны с одной переменнойСамостоятельный пример
<?php
$remaining = 3;
$data = ['left' => &$remaining, 'right' => &$remaining];
$left = ReflectionReference::fromArrayElement($data, 'left');
$right = ReflectionReference::fromArrayElement($data, 'right');
if ($left === null || $right === null) {
    throw new LogicException('В примере ожидаются две ссылки');
}
var_dump($left->getId() === $right->getId());
Ожидаемый результат
bool(true)

После проверки null стрелка -> вызывает метод описания. Сравниваем идентификаторы, не сам текст значений. Выводим только bool; печатать бинарный ID в HTML или журнал напрямую не нужно.

Одинаковые значения ещё не означают общую связьСамостоятельный пример
<?php
$firstCount = 3;
$secondCount = 3;
$data = ['first' => &$firstCount, 'second' => &$secondCount];
$first = ReflectionReference::fromArrayElement($data, 'first');
$second = ReflectionReference::fromArrayElement($data, 'second');
if ($first === null || $second === null) {
    throw new LogicException('В примере ожидаются две ссылки');
}
var_dump($first->getId() === $second->getId());
Ожидаемый результат
bool(false)

Два самостоятельных числа 3 не соединены одним =&. У элементов разные связи, хотя значения совпадают. Для диагностического человекочитаемого отображения строку можно кодировать через bin2hex, но её формат и длину не используйте как бизнес-договор.

Сначала получите описание через fromArrayElement и обработайте null. Для идентичности обычных объектов существуют другие инструменты, например строгое сравнение объектов; ReflectionReference отвечает на другой вопрос. Поиск всех ссылок в сложном графе требует отдельной политики глубины и циклов.

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

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

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

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

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