Skip to content

03 · Control Flow

Control flow statements decide which code runs, and how many times. PHP's if/loops look close to C/Java/JavaScript, plus a modern match expression introduced in PHP 8.

if / elseif / else

<?php
$score = 82;

if ($score >= 90) {
    echo "Grade: A\n";
} elseif ($score >= 80) {
    echo "Grade: B\n";
} elseif ($score >= 70) {
    echo "Grade: C\n";
} else {
    echo "Grade: F\n";
}
// Grade: B

Conditions are evaluated top to bottom; the first truthy branch runs and the rest are skipped. Remember PHP's "falsy" values: false, 0, 0.0, "", "0", [], and null are all falsy — everything else is truthy.

switch

<?php
$day = "Wed";

switch ($day) {
    case "Mon":
    case "Tue":
    case "Wed":
    case "Thu":
    case "Fri":
        echo "Weekday\n";
        break;   // without break, execution "falls through" to the next case
    case "Sat":
    case "Sun":
        echo "Weekend\n";
        break;
    default:
        echo "Not a valid day\n";
}
// Weekday

switch uses loose (==) comparison, and forgetting break is a classic bug source — execution keeps running into the next case. This is exactly what match was designed to fix.

match (PHP 8+) — a safer, expression-based alternative

<?php
$day = "Wed";

// match uses strict (===) comparison, requires no "break", and is an
// EXPRESSION -- it evaluates to a value you can assign or return directly.
$type = match ($day) {
    "Mon", "Tue", "Wed", "Thu", "Fri" => "Weekday",
    "Sat", "Sun" => "Weekend",
    default => "Not a valid day",
};

echo $type . "\n";   // Weekday

// "match (true)" + boolean arms -- a common trick for range checks,
// since match compares each arm to the subject with "===" until one is true
$score = 82;

$grade = match (true) {
    $score >= 90 => "A",
    $score >= 80 => "B",
    $score >= 70 => "C",
    default => "F",
};

echo $grade . "\n";   // B

// match with no matching arm and no default throws UnhandledMatchError
switch match
Comparison Loose (==) Strict (===)
Fall-through Yes, unless break No — each arm is isolated
Returns a value No (it's a statement) Yes (it's an expression)
No match / no default Just does nothing Throws UnhandledMatchError

while and do-while

<?php
// while: checks the condition BEFORE each iteration
$count = 0;
while ($count < 3) {
    echo "count is $count\n";
    $count++;
}
// count is 0 / count is 1 / count is 2

// do-while: runs the body ONCE, then checks -- guarantees at least one run
$attempts = 0;
do {
    echo "attempt $attempts\n";
    $attempts++;
} while ($attempts < 3);
// attempt 0 / attempt 1 / attempt 2

for loops

<?php
// for (initializer; condition; increment)
for ($i = 1; $i <= 5; $i++) {
    echo $i . " ";
}
echo "\n";
// 1 2 3 4 5

// Multiple expressions can be comma-separated in each slot
for ($i = 0, $j = 10; $i < 3; $i++, $j--) {
    echo "i=$i j=$j\n";
}
// i=0 j=10 / i=1 j=9 / i=2 j=8

foreach — the idiomatic way to loop over arrays

<?php
$fruits = ["apple", "banana", "cherry"];

// Value only
foreach ($fruits as $fruit) {
    echo $fruit . "\n";
}
// apple / banana / cherry

// Key and value
$prices = ["apple" => 1.50, "banana" => 0.75, "cherry" => 3.00];
foreach ($prices as $name => $price) {
    echo "$name costs \$$price\n";
}
// apple costs $1.5 / banana costs $0.75 / cherry costs $3

// Modify by reference with "&" -- without it, foreach works on a COPY
$numbers = [1, 2, 3];
foreach ($numbers as &$n) {
    $n *= 10;
}
unset($n);   // best practice: break the reference after the loop
print_r($numbers);   // [10, 20, 30]

foreach doesn't use an index variable you manage yourself — it's the preferred loop for arrays in idiomatic PHP, reached for far more often than for.

break and continue

<?php
foreach ([1, 2, 3, 4, 5, 6] as $n) {
    if ($n === 4) {
        break;      // exits the loop entirely
    }
    echo $n . " ";
}
echo "\n";
// 1 2 3

foreach ([1, 2, 3, 4, 5, 6] as $n) {
    if ($n % 2 === 0) {
        continue;   // skips to the next iteration
    }
    echo $n . " ";
}
echo "\n";
// 1 3 5

// A numeric argument breaks/continues out of that many nested loop levels
for ($i = 0; $i < 3; $i++) {
    for ($j = 0; $j < 3; $j++) {
        if ($j === 1) {
            continue 2;   // skips to the next iteration of the OUTER loop
        }
        echo "i=$i j=$j  ";
    }
}
echo "\n";
// i=0 j=0  i=1 j=0  i=2 j=0

Ternary and null coalescing in control flow

<?php
$age = 20;

// Ternary: condition ? if_true : if_false
$status = $age >= 18 ? "adult" : "minor";
echo $status . "\n";   // adult

// Short ternary "?:" -- returns the left side if it's truthy, else the right
$nickname = "";
$displayName = $nickname ?: "Anonymous";   // "" is falsy, so we get "Anonymous"
echo $displayName . "\n";

// Null coalescing "??" only checks for null/unset (not falsiness) -- covered
// in Module 2, but worth contrasting with "?:" here:
$value = null;
echo ($value ?? "default") . "\n";   // default
echo ($value ?: "default") . "\n";   // default -- same result here, but
                                       // "0" or "" would differ between the two

Cheat sheet

Construct Use when
if / elseif / else General branching on any condition
switch Comparing one value against several possibilities (legacy style)
match Comparing one value against several possibilities (PHP 8+, strict, expression)
while Loop while a condition holds, check-first
do-while Loop at least once, check-last
for Loop a known number of times with a counter
foreach Loop over every element of an array (idiomatic default)
break Exit the current loop (or break N for N levels)
continue Skip to the next iteration (or continue N for N levels)
?: Short ternary, falls back on falsy values
?? Null coalescing, falls back only on null/unset

How It Actually Works

Every branch you write compiles down to conditional-jump opcodes. An if becomes a ZEND_JMPZ (jump if zero/false) opcode pointing at an offset later in the opcode array; else/elseif chains are just multiple jump targets stitched together by the compiler. A switch statement compiles to a sequence of equality comparisons and jumps — it is not a jump table — which is why a switch with many cases has no algorithmic speed advantage over an equivalent if/elseif chain in PHP (unlike C, where the compiler can build a real jump table for dense integer cases). match, introduced in PHP 8, is compiler-recognized differently: it uses strict (===) comparisons internally and the engine can special-case it, but more importantly it's an expression that produces a ZEND_QM_ASSIGN-style value rather than a statement, which is why you can assign its result directly. foreach over an array doesn't walk raw memory — it uses an internal array pointer and a hash-table iterator (zend_hash_get_current_data_ex) that the engine advances each pass, iterating the array's ordered hash-table buckets rather than a contiguous C array.

🔀 See this in another language

Exercise

Write fizzbuzz.php that loops from 1 to 30 with a for loop and, for each number $i:

  • Uses match (not if/switch) to decide what to print: "FizzBuzz" if divisible by both 3 and 5, "Fizz" if divisible by 3 only, "Buzz" if divisible by 5 only, otherwise the number itself.
  • Skips printing anything for multiples of 13 using continue.
  • Stops the loop entirely once it reaches 25 using break.