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

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

Разделы этой страницы
  1. Ссылки: когда два имени изменяют одно значение
  2. Два ярлыка у одного значения: что такое ссылка PHP
  3. Изменить значение и перепривязать имя — разные действия
  4. unset снимает имя, присваивание null меняет значение
  5. Ссылка на отсутствующий элемент может сама создать его
  6. Почему функция может изменить вашу переменную
  7. Три места, где здесь встречается &
  8. Минимальный опыт: 12 стало 10
  9. Параметр по ссылке: право изменить переменную вызывающего кода
  10. return и изменение по ссылке — два канала результата
  11. Ссылка в контракте функции
  12. Перепривязка локального параметра не заменяет внешнее имя
  13. Возврат ссылки: выбрать существующее место для изменения
  14. Практическая ловушка: ссылка пережила цикл
  15. Копия массива может сохранить ссылки внутри элементов
  16. global и use (&$value): другие места появления связи
  17. Почему объекты меняются и без &
  18. Типизированное свойство сохраняет свой контракт через ссылку
  19. Как находить неочевидное изменение
  20. Самостоятельная проверка границы изменения
  21. Функции и методы этой темы
  22. Код и руководство с собой
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. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.