Free Handbook · Every example compiled & verified

Flow Control

if/elseif/else, the ternary and ??, switch and its fall-through, the strict match expression, every PHP loop, foreach by reference, and break levels.

0 / 148 lessons🔥 0 day streak
ShareXLinkedIn

Module 02 · what you'll be able to do

  • Write if / elseif / else chains and choose between a ternary, ?? and a full if
  • Explain switch fall-through and replace most switches with a strict match expression
  • Use while, do-while, for and foreach — including keys and destructuring
  • Spot and fix the foreach-by-reference bug that corrupts the last array element
  • Leave nested loops with break 2 / continue 2 and read template-style alternative syntax
01

if, elseif and else

An if runs its block when the condition is truthy. Add any number of elseif branches and an optional else; PHP checks them top to bottom and runs only the first branch whose condition is true. The condition goes in parentheses, the block in braces. Braces are optional for a single statement, but every coding standard requires them — leaving them out is how "I added a line and it always runs" bugs happen.

phpmain.php
<?php
declare(strict_types=1);

function grade(int $score): string
{
    if ($score >= 90) {
        return "A";
    } elseif ($score >= 75) {
        return "B";
    } elseif ($score >= 50) {
        return "C";
    } else {
        return "F";
    }
}

echo grade(95), " ", grade(80), " ", grade(50), " ", grade(12), "\n";

$cart = [];
if (!$cart) {                       // an empty array is falsy
    echo "Your cart is empty\n";
}

$age = 17;
$hasTicket = true;
if ($age >= 18 && $hasTicket) {
    echo "Enter\n";
} elseif ($age < 18 && $hasTicket) {
    echo "Enter with an adult\n";
}
Outputcompiled & run with real PHP
A B C F
Your cart is empty
Enter with an adult
Your turn

Add an "A+" grade for scores of 98 and above. Where must that branch go in the chain, and why?

elseif or else if?
With braces, elseif and else if behave the same (the second is an else containing an if). PSR-12 prefers elseif, and it is the only one that works with the colon syntax at the end of this module.
Return early instead of nesting
Handle the error or edge case first and return (or continue in a loop), so the main path stays at the left margin: if ($user === null) { return; }. Three levels of nested if is a code-review smell in any language.
Error you will hit

Parse error: Unclosed '{'

php
<?php
$temp = 30;
if ($temp > 25) {
    echo "hot\n";
} elseif ($temp > 15) {
    echo "warm\n";
Parse error: Unclosed '{' on line 5 in main.php on line 6
Why PHP said that

The elseif block opened on line 5 is never closed. PHP only notices at the end of the file (line 6), so the message gives you both numbers: where the brace was opened and where PHP gave up. Go to the first line number.

The fix

Close every brace you open. Editors highlight matching pairs; a formatter re-indents the file so a missing brace shows up as wrong indentation.

php
<?php
$temp = 30;
if ($temp > 25) {
    echo "hot\n";
} elseif ($temp > 15) {
    echo "warm\n";
}
02

The ternary operator and ??

When an if/else only chooses between two values, the ternary condition ? a : b says it in one expression. For "use this value, or a default if it is missing", use ?? — it checks for null or undefined without any warning. Module 01 compared ?? with the short ternary ?:; here is how they are used in real code.

phpmain.php
<?php
$stock = 0;
echo $stock > 0 ? "In stock" : "Sold out", "\n";

$count = 1;
echo "$count item" . ($count === 1 ? "" : "s"), "\n";

// ?? chains: first non-null value wins
$query = ['sort' => null];
$config = ['sort' => 'price'];
$sort = $query['sort'] ?? $config['sort'] ?? 'name';
echo "Sorting by $sort\n";

// a nested decision stays readable as if/elseif, or with brackets
$score = 75;
$band = $score > 90 ? "high" : ($score > 70 ? "mid" : "low");
echo $band, "\n";
Outputcompiled & run with real PHP
Sold out
1 item
Sorting by price
mid
Your turn

Set $config['sort'] to null too. Which value does the chain pick now?

Error you will hit

Unparenthesized nested ternary is not supported

php
<?php
$score = 75;
echo $score > 90 ? "A" : $score > 70 ? "B" : "C";
Fatal error: Unparenthesized `a ? b : c ? d : e` is not supported. Use either `(a ? b : c) ? d : e` or `a ? b : (c ? d : e)` in main.php on line 3
Stack trace:
#0 {main}
Why PHP said that

Older PHP grouped a chained ternary from the left, the opposite of every other language, so it silently gave the wrong answer. PHP 8 refuses to guess: a nested ternary without brackets is a compile-time error, and the message even shows the two possible fixes.

The fix

Add brackets around the inner ternary — or, better, use if/elseif or match (true) for three or more outcomes.

php
<?php
$score = 75;
echo $score > 90 ? "A" : ($score > 70 ? "B" : "C");
03

switch and fall-through

switch compares one value against a list of cases and jumps to the first match. Two things catch people out. First, it compares with loose ==. Second, once a case matches, PHP keeps running every following case body until it hits break — this is fall-through. Forget a break and the next case runs too.

phpmain.php
<?php
$role = 'editor';

// BUG: no break statements
switch ($role) {
    case 'admin':
        echo "full access\n";
    case 'editor':
        echo "can edit\n";
    case 'viewer':
        echo "can view\n";
}
echo "---\n";

// fixed, and grouping two cases on purpose
switch ($role) {
    case 'admin':
        echo "full access\n";
        break;
    case 'editor':
    case 'author':            // deliberate fall-through: same body
        echo "can edit\n";
        break;
    default:
        echo "can view\n";
}
Outputcompiled & run with real PHP
can edit
can view
---
can edit
Your turn

Set $role = 'admin' and predict the output of the buggy switch before you run it.

switch uses == (loose)
switch ("1") { case 1: ... } matches, because "1" == 1. For input from forms or URLs, where everything is a string, that is sometimes what you want and sometimes a hidden bug. match below compares with ===.
04

match: the strict, expression-based switch

PHP 8 added match. It is an expression — it produces a value you can assign or return — and it fixes all three problems of switch: it compares with strict ===, it has no fall-through (each arm is one expression), and if nothing matches and there is no default, it throws instead of silently doing nothing. One arm can list several values separated by commas.

phpmain.php
<?php
function statusText(int $code): string
{
    return match ($code) {
        200, 201, 204 => 'Success',
        301, 302 => 'Redirect',
        404 => 'Not found',
        default => 'Other',
    };
}

echo statusText(201), " | ", statusText(404), " | ", statusText(418), "\n";

// match(true): each arm is a condition, the first true one wins
$bmi = 22.4;
$label = match (true) {
    $bmi < 18.5 => 'underweight',
    $bmi < 25 => 'normal',
    $bmi < 30 => 'overweight',
    default => 'obese',
};
echo $label, "\n";

// strict: the string "1" does not match the int 1
echo match ("1") {
    1 => "int one",
    "1" => "string one",
}, "\n";
Outputcompiled & run with real PHP
Success | Not found | Other
normal
string one
Your turn

Add an arm for 500, 502, 503 => 'Server error' and call statusText(503).

switch

  • A statement: you assign inside each case
  • Loose == comparison
  • Falls through without break
  • No match and no default: does nothing
  • A case body can hold many statements

match

  • An expression: $x = match (...) { ... };
  • Strict === comparison
  • Never falls through
  • No match and no default: throws UnhandledMatchError
  • Each arm is one expression (call a function for more)
Error you will hit

UnhandledMatchError — no arm matched

php
<?php
$status = 404;
echo match ($status) {
    200 => 'OK',
    500 => 'Server error',
};
Fatal error: Uncaught UnhandledMatchError: Unhandled match case 404 in main.php:3
Stack trace:
#0 {main}
  thrown in main.php on line 3
Why PHP said that

match must produce a value, and no arm covers 404. Rather than return null and let the bug travel, PHP throws UnhandledMatchError, naming the value that fell through. You will also hit this when the value has the wrong type: match ("200") does not match the arm 200.

The fix

Add the missing arm, or a default arm when "anything else" is a legitimate case. Leaving out default on purpose is a good way to make unexpected values fail loudly.

php
<?php
$status = 404;
echo match ($status) {
    200 => 'OK',
    500 => 'Server error',
    default => "Unexpected status $status",
};
In real jobs
Modern PHP code reviews ask for match over switch whenever a value maps to a result. Combined with enums (Module 07), a match with no default is how teams make "we added a new status but forgot to handle it" crash in tests instead of in production.
05

while, do-while and for

while checks its condition before every pass, so the body may run zero times. do { } while (...); checks after, so the body always runs at least once. for (init; condition; step) packs a counter into one line — use it when you know how many times to loop.

phpmain.php
<?php
// while: keep going until a condition changes
$balance = 100;
$years = 0;
while ($balance < 200) {
    $balance *= 1.10;         // 10% growth per year
    $years++;
}
echo "Doubled after $years years\n";

// do-while: the body runs once even though the condition is false
$attempts = 0;
do {
    $attempts++;
    echo "Attempt $attempts\n";
} while ($attempts < 0);

// for: a counter; commas allow several expressions
for ($i = 1; $i <= 5; $i++) {
    echo $i * $i, " ";
}
echo "\n";
for ($lo = 0, $hi = 4; $lo < $hi; $lo++, $hi--) {
    echo "[$lo,$hi] ";
}
echo "\n";
Outputcompiled & run with real PHP
Doubled after 8 years
Attempt 1
1 4 9 16 25
[0,4] [1,3] 
Your turn

Rewrite the first for loop as a while loop that prints the same squares.

VisualizeStepping through a for loopStep 1 / 9
<?php
$sum = 0;
for ($i = 1; $i <= 3; $i++) {
$sum += $i;
}
echo $sum;
Line 2

$sum starts at 0.

Variables now
$sum0
All 9 steps as a table
StepLineWhat happenedVariables now
12$sum starts at 0.$sum = 0
23The init part runs once: $i = 1. The condition 1 <= 3 is true, so the body runs.$sum = 0 $i = 1
34$sum += $i$sum = 1 $i = 1
43The step $i++ runs, then the condition is checked again: 2 <= 3.$sum = 1 $i = 2
54$sum += $i$sum = 3 $i = 2
63$i++ makes 3; 3 <= 3 is still true.$sum = 3 $i = 3
74$sum += $i$sum = 6 $i = 3
83$i++ makes 4; 4 <= 3 is false, so the loop ends. Unlike many languages, $i still exists afterwards and holds 4.$sum = 6 $i = 4
96Print the total.$sum = 6 $i = 4
Infinite loops
If the condition can never become false — you forgot $years++, or you compare floats with != — the script runs until PHP's max_execution_time stops it (on the web) or forever (on the CLI, where there is no limit). Press Ctrl+C to stop a runaway CLI script.
06

foreach, keys and references

foreach is the loop you will use most: it walks an array (or any iterable object) in order. foreach ($array as $value) gives each value; foreach ($array as $key => $value) gives the key too. If each element is itself an array, you can destructure it right in the loop header.

phpmain.php
<?php
$prices = ['apple' => 30, 'bread' => 45, 'milk' => 28];

foreach ($prices as $item => $price) {
    echo str_pad($item, 6), $price, "\n";
}

$total = 0;
foreach ($prices as $price) {
    $total += $price;
}
echo "Total: $total\n";

// destructure each row in the header
$orders = [[101, 'paid'], [102, 'pending']];
foreach ($orders as [$id, $status]) {
    echo "#$id is $status\n";
}

$users = [['name' => 'Asha', 'age' => 31], ['name' => 'Ravi', 'age' => 27]];
foreach ($users as ['name' => $name, 'age' => $age]) {
    echo "$name ($age)\n";
}
Outputcompiled & run with real PHP
apple 30
bread 45
milk  28
Total: 103
#101 is paid
#102 is pending
Asha (31)
Ravi (27)

Changing elements: by reference, and the trailing-reference bug

$value in a foreach is a copy: assigning to it does not change the array. To modify elements in place, loop by reference with &$value. But after the loop, $value is still a reference to the last element. The next loop that reuses the name $value writes into that last element on every pass.

VisualizeHow a leftover reference corrupts the arrayStep 1 / 7
<?php
$nums = [1, 2, 3];
foreach ($nums as &$n) {
$n = $n * 2;
}
foreach ($nums as $n) {
}
echo implode(",", $nums);
Line 2

Start with three elements.

Variables now
$nums[1, 2, 3]
All 7 steps as a table
StepLineWhat happenedVariables now
12Start with three elements.$nums = [1, 2, 3]
24First loop, by reference: $n is an alias of each element in turn, so doubling $n doubles the element.$nums = [2, 4, 6] $n = &amp;$nums[2] (6)
35The loop ends, but $n is still an alias of $nums[2]. Nothing unset it.$nums = [2, 4, 6] $n = &amp;$nums[2] (6)
46Second loop, pass 1: PHP assigns $nums[0] (2) to $n — which writes 2 into $nums[2].$nums = [2, 4, 2] $n = &amp;$nums[2] (2)
56Pass 2: assigns $nums[1] (4) to $n, so $nums[2] becomes 4.$nums = [2, 4, 4] $n = &amp;$nums[2] (4)
66Pass 3: assigns $nums[2] to itself. It is 4 now — the original 6 is gone.$nums = [2, 4, 4] $n = &amp;$nums[2] (4)
78The last element was overwritten by the loop that "only read" the array.
phpmain.php
<?php
$nums = [1, 2, 3];
foreach ($nums as &$n) {
    $n = $n * 2;
}
unset($n);              // break the reference straight after the loop

foreach ($nums as $n) {
    // reading only
}
echo implode(",", $nums), "\n";

// often clearer: write through the key, no reference at all
$prices = [10, 20, 30];
foreach ($prices as $i => $price) {
    $prices[$i] = $price * 1.18;
}
echo implode(" ", $prices), "\n";
Outputcompiled & run with real PHP
2,4,6
11.8 23.6 35.4

Rule: every foreach (... as &$x) is followed by unset($x); — or avoid the reference and assign through the key, or build a new array with array_map() (Module 04).

07

break and continue, with levels

continue skips the rest of this pass and moves to the next one; break leaves the loop. Both take an optional number: break 2 leaves two nested loops, continue 2 skips to the next pass of the outer loop. Note that switch counts as a loop level for break and continue.

phpmain.php
<?php
for ($i = 0; $i < 6; $i++) {
    if ($i % 2 === 0) {
        continue;            // skip even numbers
    }
    echo $i, " ";
}
echo "\n";

$grid = [[1, 2, 3], [4, -1, 6], [7, 8, 9]];
foreach ($grid as $r => $row) {
    foreach ($row as $c => $value) {
        if ($value < 0) {
            echo "negative at $r,$c\n";
            break 2;         // leaves BOTH loops
        }
    }
}

$words = ['keep', 'skip', 'last'];
foreach ($words as $w) {
    switch ($w) {
        case 'skip':
            continue 2;      // 1 would only leave the switch
    }
    echo "saw $w\n";
}
Outputcompiled & run with real PHP
1 3 5
negative at 1,1
saw keep
saw last
Your turn

Change break 2 to break. The search now keeps going in row 2 — but does it print anything more? Why not?

Error you will hit

Cannot 'continue' 2 levels

php
<?php
$items = ['a', 'b'];
foreach ($items as $item) {
    if ($item === 'a') {
        continue 2;
    }
    echo $item;
}
Fatal error: Cannot 'continue' 2 levels in main.php on line 5
Stack trace:
#0 {main}
Why PHP said that

There is only one loop around the continue, so there is no second level to jump to. PHP checks this when it compiles the file, so nothing runs at all. if blocks do not count as levels — only loops and switch.

The fix

Use the number of enclosing loops you actually mean — here plain continue.

php
<?php
$items = ['a', 'b'];
foreach ($items as $item) {
    if ($item === 'a') {
        continue;
    }
    echo $item;
}
08

Alternative syntax for templates

When PHP is mixed with HTML, a lone } buried among tags is hard to match up. So if, foreach, for, while and switch have an alternative syntax: a colon opens the block and a keyword closes it — endif;, endforeach;, endfor;, endwhile;, endswitch;. You will see it in every WordPress theme and in plain-PHP views.

phpmain.php
<?php
$user = 'Asha';
$items = ['Keyboard', 'Mouse'];
?>
<h1>Hello, <?= htmlspecialchars($user) ?></h1>
<?php if ($items): ?>
<ul>
<?php foreach ($items as $item): ?>
  <li><?= htmlspecialchars($item) ?></li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<p>No items.</p>
<?php endif; ?>
Outputcompiled & run with real PHP
<h1>Hello, Asha</h1>
<ul>
  <li>Keyboard</li>
  <li>Mouse</li>
</ul>

Run on the CLI, it prints the HTML it would send to a browser. A newline straight after ?> is swallowed by PHP, which is why the tag-only lines leave no blank lines behind.

Your turn

Set $items = []; and predict the output before you run it.

Where this goes next
Laravel's Blade (@if / @foreach) and Twig ({% if %}) are template languages that compile down to exactly this kind of PHP. Module 10 builds a small page with forms and escaping.
elseif
An extra condition in an if chain; only the first true branch runs.
Ternary operator
cond ? a : b — an expression that picks one of two values. Nested ternaries need brackets in PHP 8.
Fall-through
In a switch, execution continues into the next case body until a break.
match
A PHP 8 expression that maps a value to a result using ===, with no fall-through; throws UnhandledMatchError when nothing matches.
do-while
A loop that checks its condition after the body, so it always runs at least once.
foreach by reference
foreach ($arr as &$v) — $v aliases each element so assignments change the array. unset($v) afterwards.
break N / continue N
Leave, or skip to the next pass of, the Nth enclosing loop (or switch).
Alternative syntax
if (...): ... endif; and friends — block syntax designed for templates that mix PHP and HTML.
Quick check

What does echo match ("2") { 2 => "int", default => "other" }; print?

Quick check

After foreach ($a as &$v) { } on $a = [1, 2, 3], you run $v = 9;. What is $a?

Frequently asked questions

What is the difference between switch and match in PHP?
match is a PHP 8 expression that returns a value, compares with strict ===, never falls through, and throws UnhandledMatchError when no arm matches. switch is a statement that compares with loose ==, falls through into the next case unless you write break, and silently does nothing when no case matches.
Why does foreach by reference change the last element of my array?
After foreach ($array as &$value), the variable $value is still a reference to the last element. A later loop that assigns to $value writes into that element on every pass. Call unset($value) right after the by-reference loop, or modify elements through their keys instead.
How do I break out of nested loops in PHP?
Give break a number: break 2 leaves the current loop and the one around it. continue 2 skips to the next iteration of the outer loop. A switch counts as one level.

Finish the PHP handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.