Control Structures and Loops in PHP
if/elseif/else, switch vs match, and for/foreach/while/do-while — including the loop-in-a-loop gotchas that actually bite in real code.
By DevStudio Online Team · Published September 3, 2026
The building blocks are simple; the mistakes people actually make with them are specific and worth knowing in advance.
switch vs. PHP 8's match
// switch: loose comparison, falls through without `break`
switch ($status) {
case 'active':
case 'trial':
$label = 'Live';
break;
default:
$label = 'Inactive';
}
// match: strict comparison, no fall-through, returns a value directly
$label = match ($status) {
'active', 'trial' => 'Live',
default => 'Inactive',
};match uses strict (===) comparison and throws an UnhandledMatchError if nothing matches and there's no default — which is usually what you want, since a silently-ignored switch with a missing break is one of the most common bug classes in PHP. Prefer match in any PHP 8+ codebase.
foreach by reference — the classic footgun
$numbers = [1, 2, 3];
foreach ($numbers as &$n) {
$n *= 2;
}
unset($n); // <- easy to forget, and it matters
foreach ($numbers as $n) {
$n += 100; // silently overwrites $numbers[2] because $n is still a reference!
}
print_r($numbers); // [2, 4, 106] — not [2, 4, 6] like you'd expectAfter a foreach ... as &$n loop, $n stays bound to the last element of the array by reference. If you reuse the variable name $n in a later loop without unset()-ing it first, you'll silently corrupt the array. Always unset() the reference variable immediately after a by-reference foreach.
do-while: the one loop guaranteed to run at least once
$attempts = 0;
do {
$attempts++;
$success = tryConnect();
} while (!$success && $attempts < 3);Use do-while specifically when the loop body needs to run before the condition can even be evaluated meaningfully — like a retry loop where you need the first attempt's result to decide whether to continue.
for vs. foreach
for ($i = 0; $i < count($items); $i++) { /* ... */ } // needs the index
foreach ($items as $index => $item) { /* ... */ } // needs index AND value, cleaner
foreach ($items as $item) { /* ... */ } // needs value only — prefer thisOne real performance note: for ($i = 0; $i < count($items); $i++) calls count() on every single iteration. For large arrays, cache it once — $len = count($items); — before the loop starts, or just use foreach instead, which doesn't have this problem at all.