Copy-on-Write (CoW) in PHP Arrays
debt(d7/e5/b3/t7)
Closest to 'only careful code review or runtime testing' (d7). The term's detection_hints.tools list is empty. CoW-triggered memory spikes are silent at the code level — no linter or compiler flags an accidental write inside a function receiving a large array. Discovery typically requires runtime memory profiling or careful review. A specialist memory profiler could catch it, but it won't point specifically to the CoW trigger without deliberate investigation, placing this closer to d7 than d5.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix says to make the copy explicit with '$local = $array;', which sounds like a one-liner, but the common_mistakes reveal that the real remediation involves auditing all call sites that pass large arrays to check for accidental writes, removing unnecessary & references, and restructuring loops that hold multiple references — this typically touches multiple functions or files. It is not a purely architectural rework, but it is more than a single-line swap.
Closest to 'localised tax' (b3). CoW applies broadly across web and cli contexts, but its impact is confined to code paths that work with large arrays. Most everyday code is unaffected; the burden only materialises for developers working in performance-sensitive areas. It does not shape the entire codebase architecture, so the tax is localised rather than pervasive.
Closest to 'serious trap (contradicts how a similar concept works elsewhere)' (t7). The misconception field states explicitly that passing a large array by value is believed to be expensive, when in PHP 7+ it is only expensive if the function modifies the array. This directly contradicts the mental model developers carry from most other languages (C, Java, Python) where pass-by-value of a large structure is always a copy cost. The 'obvious' assumption — that adding & avoids cost — can actually add complexity without benefit for read-only operations, and the accidental single-element append triggering a 100MB copy is a particularly counter-intuitive surprise.
Also Known As
TL;DR
Explanation
Copy-on-write is the mechanism PHP uses to make arrays behave as value types without the cost of eager copying. When you write '$b = $a' both variables point to the same array data with a shared reference count of 2. When either variable is modified — an element is added, changed, or deleted — PHP checks the reference count: if it's greater than 1, it duplicates the array data before modifying it. This copy is called a 'CoW split'. The result is that pure reads are free, but writes on shared arrays incur a full copy. Functions that receive an array and modify it (even sort(), which modifies in place) trigger a CoW split on the caller's array.
Common Misconception
Why It Matters
Common Mistakes
- Using array_push() inside a function that receives an array parameter and expecting the caller's array to change — CoW means the function works on its own copy.
- Adding & (pass by reference) to avoid CoW — this is usually unnecessary for read-only operations and adds complexity; only use & when you genuinely need to mutate the caller's variable.
- Holding multiple references to a large array in a loop — each iteration that writes triggers a CoW split per reference; restructure to minimise shared-write patterns.
- Assuming generators are always better than arrays — generators have overhead per yield; for small arrays, eager construction is simpler and often faster.
Code Examples
<?php
// ❌ Unexpected CoW split — $data is copied inside processAll() due to sort()
function processAll(array $data): array
{
sort($data); // Triggers full copy of $data — the caller's copy is unaffected
// ...but caller may not realise a 50MB copy just happened
return $data;
}
// ❌ Building a large intermediate array when a generator would do
function getAllRows(PDO $db): array
{
$rows = [];
foreach ($db->query('SELECT * FROM logs') as $row) {
$rows[] = $row; // Entire result set in memory
}
return $rows;
}
<?php
// ✅ Make the copy intent explicit when you know a write is coming
function processAll(array $data): array
{
// $data is already a CoW-detached copy once we sort it
// — explicit variable name makes it clear this is a working copy
sort($data);
return $data;
}
// ✅ Generator avoids building large intermediate array
function getAllRows(PDO $db): Generator
{
$stmt = $db->query('SELECT * FROM logs');
while ($row = $stmt->fetch()) {
yield $row; // One row in memory at a time
}
}