The Silent Loop Bug: Variable Assignment vs Array Append in PHP

📖 3 minutes read

A single character difference—one missing bracket—can cause a bug that survives months of code reviews and deployments. Here’s a PHP gotcha that’s easy to miss but painful to debug.

The Bug

Imagine you’re importing pricing data from an API. You loop through items and build a collection of Price objects:

foreach ($products->items as $item) {
    $price = $item->unitPrice;
    $prices = new Price($product->references, $price, null, null);
}

Spot the bug? It’s subtle but devastating.

That should be $prices[] = new Price(...), not $prices = new Price(...).

Without the brackets, you’re not appending to an array—you’re overwriting the variable on each iteration. After the loop completes, $prices contains only the last Price object instead of all of them.

Why It’s Dangerous

This bug pattern is particularly nasty because:

  1. It compiles successfully – PHP sees nothing wrong
  2. It’s visually subtle – Easy to miss in code review
  3. It fails at runtime – Only when downstream code enforces type safety
  4. The error manifests elsewhere – Not where the bug actually lives

Here’s what the downstream code might look like:

public function calculateTotals(array $prices): Collection
{
    return collect($prices)->map(function (Price $price) {
        return $price->calculateWithTax();
    });
}

When this runs, you’ll get:

TypeError: Argument #1 ($price) must be of type Price, array given

The error points to calculateTotals, but the actual bug is several calls upstream in the loop that builds $prices.

The Correct Pattern

Always use array append syntax when building collections in loops:

// ✅ CORRECT: Appends to array
foreach ($products->items as $item) {
    $price = $item->unitPrice;
    $prices[] = new Price($product->references, $price, null, null);
}
// Result: $prices contains ALL Price objects

If you initialize the array first, it’s even clearer:

$prices = [];
foreach ($products->items as $item) {
    $prices[] = new Price($item->references, $item->unitPrice, null, null);
}

How to Debug This

When you encounter a TypeError that points to downstream code but makes no sense (like “expected Price, got array”), trace it backward:

  1. Check the stack trace in your error monitoring (Sentry, Bugsnag, etc.)
  2. Use git blame on the lines in the stack to see recent changes
  3. Reproduce locally with the exact command from logs
  4. Add dd($prices) or var_dump($prices) right after the loop
  5. Check if you’re getting a single object instead of an array

The actual bug location is often several commits before the reported error. In one real case, this bug existed for 13 days and generated 300 Sentry events before someone traced it back through the PR history.

Always reproduce errors locally before claiming a fix works. Type errors that seem impossible often point to bugs elsewhere in the call chain.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *