PHP Conditionals and Loops

AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.

PHP 8.4

PHP control flow looks familiar, with two PHP-specific twists worth knowing early: the match expression (PHP 8, strictly better than switch in most cases) and the alternative syntax used inside templates.

if / elseif / else

$score = 85;

if ($score >= 90) {
    echo "excellent";
} elseif ($score >= 60) {
    echo "pass";
} else {
    echo "fail";
}

elseif and else if are equivalent; the one-word form is the convention.

Ternary and null coalescing:

$label = $score >= 60 ? "pass" : "fail";
$name  = $_GET["name"] ?? "anonymous";   // falls back only when null or unset

?? chains: $a ?? $b ?? "default".

Why switch is fragile

switch ($status) {
    case "pending":
        echo "waiting";
        break;
    case "done":
        echo "done";
        break;
    default:
        echo "unknown";
}

switch uses == (loose comparison) and requires a manual break — forget one and execution falls through into the next case. "1" matches 1, which is a recurring source of bugs.

match (preferred)

PHP 8’s match fixes all three: strict comparison, no fall-through, and it returns a value.

$status = "pending";

$message = match ($status) {
    "pending" => "waiting",
    "done"    => "finished",
    "failed"  => "failed",
    default   => "unknown",
};

echo $message;   // waiting

For range-style conditions, match against true:

$level = match (true) {
    $score >= 90 => "excellent",
    $score >= 60 => "pass",
    default      => "fail",
};

With no matching arm and no default, match throws UnhandledMatchError — a safer failure than switch silently skipping everything.

Rule of thumb: match for equality-style dispatch, switch only when you deliberately want fall-through.

for

Use it when the number of iterations is known:

for ($i = 0; $i < 3; $i++) {
    echo $i . "\n";
}

Do not use for to walk arrays: PHP array keys are not guaranteed to be consecutive integers, so you will miss entries or hit undefined offsets.

foreach (the default choice)

$colors = ["red", "green", "blue"];

foreach ($colors as $color) {
    echo $color . "\n";
}

foreach ($colors as $index => $color) {
    echo "$index => $color\n";
}

To modify elements, iterate by reference:

foreach ($colors as &$color) {
    $color = strtoupper($color);
}
unset($color);   // break the reference

Skipping unset leaves $color bound to the last element, so any later use of that name quietly edits the array — a nasty bug to track down.

while and do-while

while when the count is unknown:

$retry = 0;

while ($retry < 3) {
    echo "attempt " . ($retry + 1) . "\n";
    $retry++;
}

do-while always runs once:

do {
    $input = trim(fgets(STDIN));
} while ($input === "");

Before writing a while, confirm the exit condition is actually reachable.

break and continue

foreach ([1, 2, 3, 4, 5] as $n) {
    if ($n === 2) {
        continue;    // skip this iteration
    }
    if ($n === 4) {
        break;       // exit the loop
    }
    echo $n . "\n";
}
// prints 1 and 3

Both accept a level to act on enclosing loops:

foreach ($rows as $row) {
    foreach ($row as $cell) {
        if ($cell === null) {
            continue 2;   // continue the outer loop
        }
        echo $cell;
    }
}

Numeric levels are easy to miscount; past two levels, extract a function instead.

Alternative syntax for templates

Braces buried in HTML are hard to read. PHP offers a colon form with matching end* keywords:

<?php if ($loggedIn): ?>
    <p>Welcome back</p>
<?php else: ?>
    <p><a href="/login">Sign in</a></p>
<?php endif; ?>

<ul>
<?php foreach ($items as $item): ?>
    <li><?= htmlspecialchars($item, ENT_QUOTES, "UTF-8") ?></li>
<?php endforeach; ?>
</ul>
  • <?= is shorthand for <?php echo and is always available since PHP 5.4.
  • Anything printed into HTML must be escaped with htmlspecialchars — see the forms chapter.
  • The pairs are if:/endif;, foreach:/endforeach;, while:/endwhile;, for:/endfor;, and switch:/endswitch;.

Worked example: group and render

<?php
declare(strict_types=1);

$orders = [
    ["id" => 1, "status" => "pending"],
    ["id" => 2, "status" => "done"],
    ["id" => 3, "status" => "pending"],
];

$groups = [];
foreach ($orders as $order) {
    $groups[$order["status"]][] = $order["id"];
}

foreach ($groups as $status => $ids) {
    $label = match ($status) {
        "pending" => "Pending",
        "done"    => "Done",
        default   => "Other",
    };
    echo $label . ": " . implode(", ", $ids) . PHP_EOL;
}
// Pending: 1, 3
// Done: 2

Common mistakes

1. Missing break in switch

Execution continues into the next case. match removes the problem entirely.

2. Relying on switch’s loose comparison

"1" matches 1. match uses ===.

3. match without default

An unmatched value throws UnhandledMatchError. Always provide default unless throwing is intended.

4. Not unsetting a foreach reference

The last element stays aliased to your loop variable.

5. Using for on associative arrays

Keys are not consecutive; you get undefined offsets. Use foreach.

6. A while loop that never advances its variable

Infinite loop. Check the exit condition before running.

Summary

  • Prefer match for value dispatch: strict, no fall-through, returns a value.
  • Walk arrays with foreach, never for.
  • Always unset a foreach by-reference variable.
  • In templates use if: ... endif; with <?= ?> for output.

Next: functions, type declarations and scope.