Pass by value vs pass by reference — what actually gets copied
Concept
PHP is pass-by-value by default. When you pass a scalar (int, string, float, bool) to a function, PHP copies the value. The function operates on the copy; the original is untouched. Passing by reference with & gives the function an alias to the original variable — mutations inside the function affect the caller's variable directly.
For objects, the reality is subtler and frequently misunderstood. PHP does not pass objects by reference by default — it passes the object handle (a pointer-sized identifier into the object store) by value. This means the function receives its own copy of the handle, pointing to the same underlying object. Mutating the object's properties through the handle affects the original. But reassigning the variable to a new object ($obj = new Foo()) only changes the local copy of the handle — the caller still holds its own handle pointing to the original. To truly pass an object by reference (so the caller's handle variable is replaced), you need function f(Foo &$obj): void.
Copy-on-write (COW) means PHP does not actually copy scalars and arrays at the moment of passing — it defers the copy until a write occurs. If you pass a 10,000-element array to a read-only function, no copy ever happens at the memory level; PHP just increments the reference count (refcount) of the shared zval. The copy materialises only on the first write inside the function. This makes pass-by-value surprisingly cheap for read-only operations and explains why returning large arrays from functions is not inherently expensive.
References disable COW and force a hard share. A referenced variable's zval has the is_ref flag set and PHP skips COW logic entirely — any write to either alias immediately affects both. Overusing references (particularly in loop bodies) is a common performance anti-pattern because it defeats COW and forces PHP to separate zvals that it would otherwise share.
| Scenario | What gets copied |
|---|---|
| Pass scalar by value | The scalar value (COW deferred) |
| Pass array by value | Array handle (COW deferred until write) |
| Pass object by value | Object handle — not the object |
Pass by reference (&) | Nothing copied; alias established |
| Assign object to new var | New handle copy; same object |
| Pass object by reference | Alias to the handle variable |
Code Example
<?php
declare(strict_types=1);
// Scalar — function gets a copy
function increment(int $n): int
{
return ++$n;
}
$x = 5;
$y = increment($x);
echo $x . PHP_EOL; // 5 — unchanged
echo $y . PHP_EOL; // 6
// Array — COW; copy only on write
function addElement(array $arr): array
{
$arr[] = 99; // triggers actual copy here
return $arr;
}
$original = [1, 2, 3];
$modified = addElement($original);
var_dump(count($original)); // int(3) — original untouched
// By reference — scalar alias
function doubleInPlace(int &$n): void
{
$n *= 2;
}
$val = 10;
doubleInPlace($val);
echo $val . PHP_EOL; // 20
// Object handle — method mutation affects original
class Counter
{
public int $count = 0;
public function increment(): void { $this->count++; }
}
function mutateCounter(Counter $c): void
{
$c->increment(); // mutates the shared object
$c = new Counter(); // only replaces LOCAL handle; caller unaffected
}
$counter = new Counter();
mutateCounter($counter);
echo $counter->count . PHP_EOL; // 1 — method call landed; reassign did not
// Object by reference — caller's handle replaced
function replaceCounter(Counter &$c): void
{
$c = new Counter();
$c->count = 42;
}
replaceCounter($counter);
echo $counter->count . PHP_EOL; // 42 — caller now holds new objectInterview Q&A
Q: PHP is described as "pass objects by reference" in many tutorials. Is this accurate?
No, it is a common misconception. PHP passes a copy of the object handle (identifier), not a reference to the variable holding the handle. Two distinct but important consequences follow. First, mutating the object through the handle inside the function — calling methods, setting properties — affects the original object that the caller holds. Second, reassigning the local parameter to a completely new object does not affect the caller's variable; only the local copy of the handle changes. True pass-by-reference (function f(MyClass &$obj)) would allow the function to replace the caller's variable with a new object.
Q: What is copy-on-write and how does it affect the cost of passing large arrays to functions?
COW is PHP's lazy copying mechanism. When an array is passed by value, PHP increments the reference count of the shared zval bucket rather than allocating and copying all the data. A physical copy of the array's hash table is only triggered the first time the function performs a write — adding a key, modifying a value, or unsetting an element. If a function only reads the array (e.g., iterates to compute a sum), no copy ever occurs. This means passing a 100,000-element array to a pure read function has essentially zero copying overhead. However, as soon as you write, PHP separates the arrays and both the caller and callee now own independent copies.
Q: When would you pass by reference in production code, and when should you avoid it?
Use pass-by-reference sparingly and for specific semantic reasons: (1) built-in functions that modify in place require it (preg_match captures, array_shift); (2) when you need to return multiple values and do not want to return an array or object wrapper (though in PHP 8 you usually prefer returning a typed object or a tuple via array destructuring); (3) swap utilities. Avoid references in: loop iterations over large collections (defeats COW), anywhere a value type is expected by the reader (breaks local reasoning), and as a substitute for proper return types. In Laravel's codebase, references appear almost nowhere in application-layer code — the container, the pipeline, and collections all use return values, making code testable and readable.