Формально масив у PHP передається за значенням, але фізичного копіювання під час виклику не відбувається. Усередині масив - це структура zend_array із власним лічильником посилань. Присвоєння $b = $a, передача аргументом і повернення з функції збільшують цей лічильник, а самі дані лишаються в одному екземплярі. Механізм називають copy-on-write: PHP відкладає копію до моменту, коли хтось із власників спробує щось записати. Тоді відбувається separation - для того, хто пише, створюється власна структура, і далі дві змінні живуть незалежно.
Практичний наслідок видно в першому блоці коду. Виклик countRows($rows) на масиві зі ста тисяч елементів не додає жодного байта, бо функція тільки читає. А appendRow($rows), який дописує один елемент, коштує повного дубля масиву в памʼяті: у момент запису на дані дивилися і зовнішня змінна, і параметр. Звідси робоче правило: боятися треба не передачі великих масивів, а модифікації всередині функції. Якщо обробляти доводиться щось справді велике, вихід не в &$rows, а в тому, щоб не тримати весь набір у памʼяті: генератор із yield, курсор до бази, читання файла порціями.
Амперсанд у foreach - окрема історія, на якій ловлять найчастіше. Запис foreach ($items as &$value) робить $value посиланням на поточний елемент, і після завершення циклу це посилання не зникає: змінна далі вказує на останній елемент масиву. Наступний звичайний foreach ($items as $value) починає присвоювати в неї значення одне за одним, послідовно затираючи хвіст, і масив ['A','B','C'] тихо стає ['A','B','B']. Помилка не дає ні попередження, ні винятку. Лікується вона одним рядком unset($value) одразу після циклу. Є й глибший ефект: масив, елементи якого стали посиланнями, втрачає швидке копіювання, і при «копіюванні» такого масиву елементи-посилання лишаються спільними для обох копій.
З обʼєктами модель інша, і сформульована вона рівно однією фразою: змінна тримає не сам обʼєкт, а його ідентифікатор. Передача обʼєкта копіює цей ідентифікатор, копія вказує на той самий екземпляр, ніякого copy-on-write тут немає. Тому addItem($cart) у прикладі дописує елемент у масив, який реально побачить зовнішній код, хоча & у сигнатурі не стоїть. Межа проходить по присвоєнню самої змінної: $cart = new Cart() усередині функції зовні не видно, бо ви замінили локальну копію ідентифікатора, а не сам обʼєкт. Щоб зовнішня змінна побачила підміну, потрібен явний &$cart у параметрі.
Вибір між масивом і обʼєктом уже не про швидкість передачі: у PHP 7+ обидва варіанти коштують інкремента лічильника. Питання в тому, яку семантику ви хочете дати виклику. Масив-аргумент дає ізоляцію: функція не зіпсує вхідні дані, поки не запише в них, а якщо запише, зміну доведеться повернути результатом. Обʼєкт-аргумент дає спільний стан і мовчазні побічні ефекти, тому такі функції краще називати дієсловом (addItem, applyDiscount), а не getSomething. Якщо ж кортеж даних передається між шарами й змінюватися не має, надійніше загорнути його в readonly-клас: тоді про copy-on-write і про випадкові записи думати вже не доведеться.
declare(strict_types=1);
function countRows(array $rows): int
{
return count($rows); // лише читаємо: копії не буде взагалі
}
function appendRow(array $rows): array
{
$rows[] = ['id' => 0]; // перший запис - тут народжується копія
return $rows;
}
$rows = array_fill(0, 100_000, ['id' => 1]);
$before = memory_get_usage();
countRows($rows);
echo memory_get_usage() - $before; // близько нуля: передали лічильник, не дані
$before = memory_get_usage();
appendRow($rows); // а тут памʼять стрибне на розмір масиву
echo memory_get_usage() - $before;
// foreach з & лишає $value посиланням на останній елемент
$letters = ['a', 'b', 'c'];
foreach ($letters as &$value) {
$value = strtoupper($value);
}
unset($value); // без цього наступний цикл зіпсує хвіст
foreach ($letters as $value) {
// якби не unset вище, тут вийшло б ['A', 'B', 'B']
}
print_r($letters); // ['A', 'B', 'C']
final class Cart
{
/** @var list<string> */
public array $items = [];
}
function addItem(Cart $cart): void
{
$cart->items[] = 'книга'; // той самий обʼєкт, зміну видно назовні
}
$cart = new Cart();
addItem($cart);
echo count($cart->items); // 1
Формулювання, яке закриває питання: «масив - значення з copy-on-write: передача коштує інкремента лічильника, а копія народжується на першому записі; обʼєкт copy-on-write не має, бо змінна тримає його ідентифікатор». Далі одразу згадайте `unset()` після `foreach` з амперсандом: це показує, що ви на цьому вже обпікалися.