PHP Control Flow — The Complete Guide with Real-World Examples
- PHP evaluates if/elseif/else top-to-bottom and stops the moment a condition is true — order your conditions from most specific to most general or you'll get wrong results.
- Always add break to switch cases unless you deliberately want fall-through between adjacent cases sharing the same outcome — missing break is one of the most common silent PHP bugs in production.
- Choose your loop by what you know: for when you know the count upfront, while when you don't, foreach when you have an array. Using the wrong loop type makes your intent harder to read.
Control flow is the traffic system of your code. It decides which parts run, when they run, and whether they run at all. Without control flow, every line would execute in order, no matter what. With it, you can say: 'if this user is logged in, show their dashboard; otherwise, show the login page' — or 'for each item in the cart, calculate its total.' That's control flow. Think of it like a recipe. A recipe doesn't just list ingredients — it has branches: 'If the dough is too dry, add a tablespoon of water.' It has loops: 'Knead for 10 minutes.' And it has multiple paths: 'If you have a stand mixer, use it; otherwise, knead by hand.' That's exactly what control flow does for your code.
PHP started as a simple templating language, but today it powers over 75% of the web — from WordPress to Laravel to Facebook's Hack derivative. At the core of every PHP application lies control flow: the logic that decides which code runs and when. If you can't control the flow of execution, you can't build anything dynamic.
I've debugged PHP applications for over a decade, from small WordPress plugins to multi-million request-per-minute e-commerce platforms. The bugs that cause the most damage are almost never complex algorithms. They're simple control flow mistakes — a missing break in a switch, a loose comparison where strict was needed, a variable that silently changes type, a loop that iterates one too many times.
In one memorable incident, a missing break in a payment processing switch caused three customers to receive shipping confirmations for orders they hadn't paid for. The code looked perfectly correct. The developer had written a switch statement for payment status: pending, processing, completed, failed. They forgot the break after the 'pending' case. When a pending payment was processed, it matched 'pending', then fell through to 'completed', then 'shipped'. The system thought the payment was both pending and completed and sent the shipping confirmation. All because of one missing word.
That's what this guide is about. Not the theory of control flow, but how to use it safely in production. You'll learn the difference between if and switch, when to use a for loop vs foreach, why match in PHP 8 is a game-changer, and the one trick with references that corrupts arrays silently. We'll cover ternary operators, null coalescing, and the subtle type juggling traps that catch even experienced developers. By the end, you'll be able to read any PHP control structure, know which one to use in every situation, and avoid the mistakes that cost real money in production.
if, elseif, else — The Backbone of Decision Making
The if statement is the simplest and most common control structure. It lets you execute code conditionally: if a condition is true, do something; otherwise (optionally) do something else.
PHP evaluates conditions from top to bottom. The moment it finds a true condition, it executes that block and skips the rest of the if/elseif/else ladder. This order matters. If you put a general condition before a specific one, the specific one never runs.
Example: checking a student's score. If you check 'score >= 60' (pass) before 'score >= 90' (distinction), every distinction student also passes the first check, so they never reach the distinction branch. Order matters: most specific conditions first, most general last.
In production, I've seen this exact bug in discount code logic. A developer wrote if ($discount >= 10) before if ($discount >= 20). The 20% discount code matched the 10% branch first, so customers got 10% off instead of 20%. The fix was reversing the order.
elseis always optional- You can chain as many
elseifblocks as you need elsemust always be the last block if it exists- Conditions are evaluated in order, first true condition wins
- Always use strict comparison (
===) unless you specifically need type juggling
<?php // io.thecodeforge: If/Elseif/Else — Grade Evaluator function evaluateGrade($score) { echo "Score: $score — "; if ($score >= 90) { echo "Distinction: Excellent work!" . PHP_EOL; } elseif ($score >= 75) { echo "First Class: Very good!" . PHP_EOL; } elseif ($score >= 60) { echo "Second Class: Good, but keep improving." . PHP_EOL; } elseif ($score >= 35) { echo "Pass: Just passed. Work harder next time." . PHP_EOL; } else { echo "Fail: Needs improvement." . PHP_EOL; } } evaluateGrade(95); // Distinction evaluateGrade(82); // First Class evaluateGrade(67); // Second Class evaluateGrade(42); // Pass evaluateGrade(28); // Fail // ORDER MATTERS — WRONG EXAMPLE function wrongGradeOrder($score) { echo "Score: $score (WRONG ORDER) — "; if ($score >= 60) { echo "Pass" . PHP_EOL; // This catches everything >= 60 } elseif ($score >= 90) { echo "Distinction" . PHP_EOL; // NEVER runs! } } wrongGradeOrder(95); // Outputs 'Pass' instead of 'Distinction'
Score: 82 — First Class: Very good!
Score: 67 — Second Class: Good, but keep improving.
Score: 42 — Pass: Just passed. Work harder next time.
Score: 28 — Fail: Needs improvement.
Score: 95 (WRONG ORDER) — Pass
The Classic Switch Mistake — When You Forget break
The switch statement is designed for situations where you're comparing a single variable against many possible values. Think of it as a cleaner alternative to a long if/elseif chain.
The structure: switch ($variable) with multiple case blocks. Each case is a value to match. If the variable matches that value, PHP executes all code from that case until it hits a break statement. Without break, PHP continues executing into the next case — this is called "fall-through".
Fall-through is sometimes intentional. You can group multiple cases that should execute the same code. But unintentional fall-through is one of the most common PHP bugs in production.
I mentioned a payment processor bug earlier. Here's exactly what happened: the developer wrote:
``php switch ($paymentStatus) { case 'pending': processPayment(); case 'completed': sendConfirmationEmail(); case 'failed': logFailure(); break; } ``
When $paymentStatus = 'pending', PHP matched the 'pending' case, ran processPayment(), then — because there was no break — fell through to the 'completed' case and ran sendConfirmationEmail(), then fell through to 'failed' and ran logFailure(). One pending payment triggered three different code paths.
The fix: add break after each case that shouldn't fall through. The only time you omit break is when multiple cases intentionally share the same code block.
In PHP 8+, the new match expression solves this by never falling through — but switch remains in legacy codebases and sometimes intentional fall-through is exactly what you want.
<?php // io.thecodeforge: Switch Statement with Intentional vs Accidental Fall-through function checkDeliveryStatus($statusCode) { echo "Status $statusCode: "; switch ($statusCode) { case 1: echo "Order placed — "; // Intentional fall-through: case 1 and 2 both proceed to 'processing' case 2: echo "Processing — "; // Intentional fall-through into case 3 case 3: echo "Shipped"; break; case 4: echo "Out for delivery"; break; case 5: echo "Delivered"; break; default: echo "Unknown status"; } echo PHP_EOL; } checkDeliveryStatus(1); // Order placed — Processing — Shipped checkDeliveryStatus(2); // Processing — Shipped checkDeliveryStatus(3); // Shipped checkDeliveryStatus(4); // Out for delivery checkDeliveryStatus(5); // Delivered checkDeliveryStatus(99); // Unknown status // ACCIDENTAL FALL-THROUGH (the payment processor bug) function paymentProcessor($status) { echo "Payment $status: "; switch ($status) { case 'pending': echo "Processing payment — "; // MISSING BREAK — disaster! case 'completed': echo "Sending confirmation — "; // MISSING BREAK case 'failed': echo "Logging failure."; break; } echo PHP_EOL; } paymentProcessor('pending'); // Processing payment — Sending confirmation — Logging failure. paymentProcessor('completed'); // Sending confirmation — Logging failure. paymentProcessor('failed'); // Logging failure.
Status 2: Processing — Shipped
Status 3: Shipped
Status 4: Out for delivery
Status 5: Delivered
Status 99: Unknown status
Payment pending: Processing payment — Sending confirmation — Logging failure.
Payment completed: Sending confirmation — Logging failure.
Payment failed: Logging failure.
break was invisible in code review because the code looked fine at a glance. Make it a habit: every case gets a break unless you're intentionally grouping multiple cases together. When you do intend fall-through, add a comment explaining why so future developers don't 'fix' it.Loops — When to Use for, while, foreach, and do-while
Loops are the workhorses of PHP. They let you repeat code without writing it multiple times. PHP gives you four types, each with a different job:
for — Use when you know exactly how many times you need to loop. The classic for ($i = 0; $i < 10; $i++) runs 10 times. Perfect for counters, pagination, and processing fixed-size collections.
while — Use when you don't know the count upfront. It checks the condition BEFORE each iteration. If the condition starts false, the loop body never runs. Great for reading database results, processing files until EOF, and retry loops with an unknown number of attempts.
do-while — Like while, but checks the condition AFTER the loop body. The loop always runs at least once. Perfect for menu systems where you want to show the user an option at least once before asking if they want to continue.
foreach — The most common loop in modern PHP. It iterates over arrays and objects, automatically handling keys and values. Use foreach whenever you're working with collections — it's cleaner and less error-prone than manually managing indices with for.
continue and break inside loopscontinueskips the rest of the current iteration and moves to the next onebreakexits the loop entirely
In production, I've seen developers use break when they meant continue, causing processing to stop prematurely. Use break to exit early when you've found what you're looking for. Use continue to skip invalid items while processing the rest.
<?php // io.thecodeforge: The Four Loop Types echo "=== FOR: Multiplication Table ===" . PHP_EOL; $number = 5; for ($i = 1; $i <= 10; $i++) { echo "$number x $i = " . ($number * $i) . PHP_EOL; } echo PHP_EOL . "=== WHILE: Countdown Until Condition Met ===" . PHP_EOL; $countdown = 5; while ($countdown > 0) { echo "T-minus $countdown seconds..." . PHP_EOL; $countdown--; } echo "Liftoff!" . PHP_EOL; echo PHP_EOL . "=== DO-WHILE: Runs at Least Once ===" . PHP_EOL; $attempts = 0; do { $attempts++; echo "Attempt $attempts: Connecting to service..." . PHP_EOL; } while ($attempts < 3 && rand(0, 1) === 0); // Simulates random success echo "Connection established after $attempts attempt(s)." . PHP_EOL; echo PHP_EOL . "=== FOREACH: Processing Collection ===" . PHP_EOL; $items = ['Apple', 'Banana', 'Cherry', 'Date']; foreach ($items as $index => $item) { echo "Item $index: $item" . PHP_EOL; } echo PHP_EOL . "=== break vs continue ===" . PHP_EOL; $numbers = range(1, 10); echo "Using continue (skip even numbers): "; foreach ($numbers as $num) { if ($num % 2 == 0) { continue; // skip this iteration } echo "$num "; } echo PHP_EOL; echo "Using break (stop at 5): "; foreach ($numbers as $num) { if ($num > 5) { break; // exit loop entirely } echo "$num "; } echo PHP_EOL;
5 x 1 = 5
5 x 2 = 10
5 x 3 = 15
5 x 4 = 20
5 x 5 = 25
5 x 6 = 30
5 x 7 = 35
5 x 8 = 40
5 x 9 = 45
5 x 10 = 50
=== WHILE: Countdown Until Condition Met ===
T-minus 5 seconds...
T-minus 4 seconds...
T-minus 3 seconds...
T-minus 2 seconds...
T-minus 1 seconds...
Liftoff!
=== DO-WHILE: Runs at Least Once ===
Attempt 1: Connecting to service...
Connection established after 1 attempt(s).
=== FOREACH: Processing Collection ===
Item 0: Apple
Item 1: Banana
Item 2: Cherry
Item 3: Date
=== break vs continue ===
Using continue (skip even numbers): 1 3 5 7 9
Using break (stop at 5): 1 2 3 4 5
foreach for collections (most common in modern PHP). Use for when you need a counter. Use while when you're waiting for a condition that might never happen. Use do-while when the code must run at least once. Picking the right loop communicates your intent to other developers more clearly than using a for loop for everything.PHP's Type Juggling in Conditionals — The Truthy/Falsy Trap
PHP is a loosely typed language. This means it automatically converts values between types when comparing them. This 'type juggling' is convenient in simple cases but dangerous in conditionals.
The falsy values in PHP — values that evaluate to false in a boolean context: - false (the boolean) - 0 (integer zero) - 0.0 (float zero) - '' (empty string) - '0' (the string '0' — this catches people off guard) - null - [] (empty array)
'0'is falsy, but'0.0'is truthy'false'(the word) is truthy — it's a non-empty string[]is falsy, but[null](array containing null) is truthy
This is why === (strict comparison) is always safer than == (loose comparison). With ==, PHP juggles types before comparing. With ===, it checks both value AND type — no juggling.
I once debugged a login system where if ($_POST['admin'] == true) was granting admin access when the string '0' was submitted, because '0' == true evaluates to... well, actually '0' is falsy so '0' == false is true. The real issue was the developer expected 'false' to be falsy — but 'false' is a non-empty string, so it's truthy. The fix was === everywhere.
Use to check if a variable exists. Use isset() to check if a variable exists AND is truthy. Use empty()=== for all comparisons in conditionals. These three habits prevent 90% of type-related bugs in PHP.
<?php // io.thecodeforge: PHP Type Juggling in Conditionals // Shows the difference between == (loose) and === (strict) echo "=== Loose Comparison (==) Surprises ===" . PHP_EOL; // These all evaluate to TRUE with == — most are surprising var_dump(0 == '0'); // true — int 0 equals string '0' var_dump(0 == ''); // true — int 0 equals empty string var_dump(0 == false); // true — int 0 equals boolean false var_dump('0' == false); // true — string '0' equals boolean false var_dump('' == false); // true — empty string equals boolean false var_dump(null == false); // true — null equals boolean false var_dump('false' == false); // FALSE — 'false' is a non-empty string (truthy!) echo PHP_EOL . "=== Strict Comparison (===) — Always Safe ===" . PHP_EOL; var_dump(0 === '0'); // false — int is not string var_dump(0 === ''); // false — int is not string var_dump(0 === false); // false — int is not bool var_dump('0' === false); // false — string is not bool var_dump(null === false); // false — null is not bool echo PHP_EOL . "=== isset() vs empty() ===" . PHP_EOL; $undefinedVariable = null; // isset: does the variable exist and is not null? var_dump(isset($undefinedVariable)); // false — it's null // empty: is the value falsy? (includes '', 0, null, false, [], '0') var_dump(empty($undefinedVariable)); // true — null is empty var_dump(empty(0)); // true — 0 is empty var_dump(empty('0')); // true — '0' is empty var_dump(empty('hello')); // false — non-empty string is not empty echo PHP_EOL . "=== Safe Pattern: Always Use ===" . PHP_EOL; $loginInput = '0'; // User submitted the string '0' // DANGEROUS: loose comparison if ($loginInput == true) { echo "Loose: '$loginInput' is truthy (WRONG for login checks!)" . PHP_EOL; } // SAFE: strict comparison if ($loginInput === true) { echo "Strict: '$loginInput' is truthy" . PHP_EOL; } else { echo "Strict: '$loginInput' is NOT boolean true (correct!)" . PHP_EOL; }
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(false)
=== Strict Comparison (===) — Always Safe ===
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
=== isset() vs empty() ===
bool(false)
bool(true)
bool(true)
bool(true)
bool(false)
=== Safe Pattern: Always Use ===
Loose: '0' is truthy (WRONG for login checks!)
Strict: '0' is NOT boolean true (correct!)
if ($value) checks even though it's a perfectly valid value. I've seen production bugs where order_id='0' was treated as 'no order' because the developer used if ($orderId) instead of if ($orderId !== null && $orderId !== ''). Always use isset() or explicit null checks, not truthiness, for values that could legitimately be zero.The switch Statement — When You Have Many Specific Cases to Handle
You've mastered if/elseif/else — but imagine you're building a day-of-week scheduler and you need seven specific outcomes. Writing seven elseif blocks works, but it starts to look like a wall of text that's hard to scan.
The switch statement was invented exactly for this situation. It takes a single value, and then lets you list specific cases to match against it. Think of it like a hotel receptionist looking up a room number: they don't check 'is this room greater than 100? Is it less than 200?' They go straight to the register and look up the exact number.
switch compares using loose equality (==), so it matches type-flexibly. Each case must end with a break statement — without it, PHP keeps running into the next case like a runaway train (this is called 'fall-through'). The default case at the bottom is your safety net, just like else.
Use switch when you're comparing ONE variable against MANY specific values. Use if/elseif when your conditions involve ranges, multiple variables, or complex logic.
The missing-break bug I mentioned in the introduction: a developer wrote a payment status switch without breaks. When a 'pending' payment came in, it matched the 'pending' case, then fell through to 'completed,' then 'shipped.' The system sent a shipping confirmation for a payment that hadn't been processed. Three customers got 'your order has shipped' emails for items they hadn't paid for. All because of one missing break.
<?php // io.thecodeforge: Switch Statement with Intentional vs Accidental Fall-through function checkDeliveryStatus($statusCode) { echo "Status $statusCode: "; switch ($statusCode) { case 1: echo "Order placed — "; // Intentional fall-through: case 1 and 2 both proceed to 'processing' case 2: echo "Processing — "; // Intentional fall-through into case 3 case 3: echo "Shipped"; break; case 4: echo "Out for delivery"; break; case 5: echo "Delivered"; break; default: echo "Unknown status"; } echo PHP_EOL; } checkDeliveryStatus(1); // Order placed — Processing — Shipped checkDeliveryStatus(2); // Processing — Shipped checkDeliveryStatus(3); // Shipped checkDeliveryStatus(4); // Out for delivery checkDeliveryStatus(5); // Delivered checkDeliveryStatus(99); // Unknown status // ACCIDENTAL FALL-THROUGH (the payment processor bug) function paymentProcessor($status) { echo "Payment $status: "; switch ($status) { case 'pending': echo "Processing payment — "; // MISSING BREAK — disaster! case 'completed': echo "Sending confirmation — "; // MISSING BREAK case 'failed': echo "Logging failure."; break; } echo PHP_EOL; } paymentProcessor('pending'); // Processing payment — Sending confirmation — Logging failure. paymentProcessor('completed'); // Sending confirmation — Logging failure. paymentProcessor('failed'); // Logging failure.
Status 2: Processing — Shipped
Status 3: Shipped
Status 4: Out for delivery
Status 5: Delivered
Status 99: Unknown status
Payment pending: Processing payment — Sending confirmation — Logging failure.
Payment completed: Sending confirmation — Logging failure.
Payment failed: Logging failure.
break was invisible in code review because the code looked fine at a glance. Make it a habit: every case gets a break unless you're intentionally grouping multiple cases together. When you do intend fall-through, add a comment explaining why so future developers don't 'fix' it.Nested Control Flow and the Foreach Reference Trap
Real PHP code rarely uses a single if or a single loop in isolation. In production, you nest them — an if inside a foreach, a foreach inside a while, a switch inside an if. This is where control flow gets powerful, and also where bugs hide.
The most common production pattern: iterate over a collection (foreach) and make a decision about each item (if). This is how you process shopping carts, validate form inputs, filter database results, and build API responses.
But nesting comes with a trap that catches even experienced developers: the foreach reference trap. When you use & (reference) in a foreach to modify array values in place, the variable retains its reference after the loop ends. If you reuse that variable name in a subsequent foreach, you silently corrupt your array.
This bug is subtle because the code looks perfectly correct. The first loop modifies values as intended. The second loop looks innocent. But the $value variable from the first loop still points to the last array element, so every iteration of the second loop overwrites that last element. I've debugged this exact pattern in production — a data processing pipeline that was silently losing the last item in every batch because a developer reused a reference variable across two foreach loops.
<?php // io.thecodeforge: Nested Control Flow Examples echo "=== Shopping Cart Validation (Nested Loop + Conditions) ===" . PHP_EOL; $cart = [ ['name' => 'Widget A', 'price' => 25.00, 'quantity' => 3], ['name' => 'Widget B', 'price' => 0, 'quantity' => 1], ['name' => 'Widget C', 'price' => 49.99, 'quantity' => 0], ['name' => 'Widget D', 'price' => 99.99, 'quantity' => 2], ]; $validItems = []; $totalPrice = 0; foreach ($cart as $item) { // Skip items with zero or negative quantity if ($item['quantity'] <= 0) { echo "Skipped: {$item['name']} (quantity is {$item['quantity']})" . PHP_EOL; continue; } // Skip items with zero price (likely a data error) if ($item['price'] <= 0) { echo "Skipped: {$item['name']} (price is {$item['price']})" . PHP_EOL; continue; } // Item is valid — add to cart total $lineTotal = $item['price'] * $item['quantity']; $validItems[] = $item; $totalPrice += $lineTotal; echo "Added: {$item['name']} x{$item['quantity']} = $" . number_format($lineTotal, 2) . PHP_EOL; } echo "Cart total: $" . number_format($totalPrice, 2) . PHP_EOL; echo PHP_EOL; // THE FOREACH REFERENCE TRAP echo "=== Foreach Reference Trap ===" . PHP_EOL; $colors = ['red', 'green', 'blue']; // First loop: modify values using reference (&) foreach ($colors as &$color) { $color = strtoupper($color); } // BUG: $color is STILL a reference to the LAST element ($colors[2]) // At this point: $colors = ['RED', 'GREEN', 'BLUE'] // But $color points to $colors[2] which is 'BLUE' // Second loop: reusing $color without realizing it's still a reference foreach ($colors as $color) { // Each iteration assigns to $color, which still points to $colors[2] // So $colors[2] gets overwritten on EVERY iteration } echo "After trap: "; var_dump($colors); // Expected: ['RED', 'GREEN', 'BLUE'] // Actual: ['RED', 'GREEN', 'RED'] — last element is corrupted! // FIX: unset the reference after the first loop $colors2 = ['red', 'green', 'blue']; foreach ($colors2 as &$color2) { $color2 = strtoupper($color2); } unset($color2); // CRITICAL: breaks the reference foreach ($colors2 as $color2) { // Safe now — $color2 is a normal variable, not a reference } echo "After fix: "; var_dump($colors2); // Correct: ['RED', 'GREEN', 'BLUE']
Added: Widget A x3 = $75.00
Skipped: Widget B (price is 0)
Skipped: Widget C (quantity is 0)
Added: Widget D x2 = $199.98
Cart total: $274.98
=== Foreach Reference Trap ===
After trap: array(3) { [0]=> string(3) "RED" [1]=> string(5) "GREEN" [2]=> string(3) "RED" }
After fix: array(3) { [0]=> string(3) "RED" [1]=> string(5) "GREEN" [2]=> string(4) "BLUE" }
& (reference), immediately call unset($variable) to break the reference. This is not optional — it's a production safety requirement. Without it, any subsequent use of that variable name silently corrupts your array. I've seen this bug cause data pipeline corruption that took weeks to trace because the affected code looked perfectly normal.The Ternary Operator and match — Cleaner Control Flow for Simple Decisions
Once you're comfortable with if/else, PHP gives you two powerful shortcuts for situations where your decisions are simple and the code would otherwise be verbose.
The ternary operator ? : condenses a simple if/else into a single line. The structure reads: condition ? value_if_true : value_if_false. It's not about saving lines for its own sake — it genuinely improves readability when the decision is straightforward. But if you need to nest ternaries inside each other, stop. Use a regular if/else. Nested ternaries are nearly impossible to read, and in PHP 8, deeply nested ternaries may trigger deprecation warnings.
The null coalescing operator ?? is a special ternary for null checks. $_GET['name'] ?? 'Guest' means 'use $_GET['name'] if it exists and is not null, otherwise use 'Guest''. It's the single most useful operator for handling optional form data, API responses, and configuration values.
The match expression (introduced in PHP 8.0) is a modern, stricter version of switch. The key differences: match uses strict comparison (===), it doesn't fall through between arms (no break needed), and it returns a value directly, so you can assign the result to a variable. It also throws an UnhandledMatchError if no arm matches and there's no default — which is actually a feature, not a bug, because it forces you to handle every case.
Use ternary for simple true/false assignments. Use null coalescing for optional values with defaults. Use match for value-based branching in PHP 8+ code.
<?php // io.thecodeforge: Ternary, Null Coalescing, and Match echo "=== Ternary Operator (if/else shortcut) ===" . PHP_EOL; $cartCount = 3; $message = $cartCount > 0 ? "You have $cartCount items in your cart." : "Your cart is empty."; echo $message . PHP_EOL; $cartCount = 0; $message = $cartCount > 0 ? "You have $cartCount items in your cart." : "Your cart is empty."; echo $message . PHP_EOL; echo PHP_EOL . "=== Null Coalescing Operator (??) — Safe Defaults ===" . PHP_EOL; // Without null coalescing $name = isset($_GET['name']) ? $_GET['name'] : 'Guest'; echo "Hello, $name!" . PHP_EOL; // With null coalescing (much cleaner) $name = $_GET['name'] ?? 'Guest'; echo "Hello, $name!" . PHP_EOL; // Chaining multiple fallbacks $config = ['theme' => null, 'language' => 'en']; $theme = $config['theme'] ?? $config['default_theme'] ?? 'light'; echo "Theme: $theme" . PHP_EOL; echo PHP_EOL . "=== MATCH EXPRESSION (PHP 8.0+) ===" . PHP_EOL; $httpStatusCode = 404; $statusDescription = match($httpStatusCode) { 200 => 'OK — Request succeeded.', 201 => 'Created — Resource successfully created.', 301 => 'Moved Permanently — Redirect to new URL.', 400 => 'Bad Request — Check your request parameters.', 401 => 'Unauthorized — Authentication required.', 403 => 'Forbidden — You do not have permission.', 404 => 'Not Found — This resource does not exist.', 500 => 'Internal Server Error — Something went wrong.', default => 'Unknown status code.' }; echo "HTTP $httpStatusCode: $statusDescription" . PHP_EOL; // Match with multiple values per arm $currentMonth = 8; $season = match(true) { in_array($currentMonth, [12, 1, 2]) => 'Winter', in_array($currentMonth, [3, 4, 5]) => 'Spring', in_array($currentMonth, [6, 7, 8]) => 'Summer', in_array($currentMonth, [9, 10, 11]) => 'Autumn', default => 'Unknown' }; echo "Month $currentMonth is in $season." . PHP_EOL;
You have 3 items in your cart.
Your cart is empty.
=== Null Coalescing Operator (??) — Safe Defaults ===
Hello, Guest!
Hello, Guest!
Theme: light
=== MATCH EXPRESSION (PHP 8.0+) ===
HTTP 404: Not Found — This resource does not exist.
Month 8 is in Summer.
match expression is strictly safer than switch because it uses === (strict type comparison), throws an error for unhandled cases instead of silently doing nothing, and returns a value you can assign directly. If you're writing new PHP 8+ code, match should be your default choice for value-based branching. Reserve switch for legacy codebases that can't upgrade to PHP 8.| Feature / Aspect | switch | match (PHP 8+) |
|---|---|---|
| Comparison type | Loose (==) — '1' matches 1 | Strict (===) — '1' does NOT match 1 |
| Fall-through behavior | Yes — must use break to prevent it | No — each arm is isolated automatically |
| Returns a value | No — executes statements only | Yes — returns a value, assignable to variable |
| Unmatched case handling | Silently does nothing (risky) | Throws UnhandledMatchError (safer) |
| Multiple values per case | Need separate cases or fall-through | Can use arrays directly: case 1,2,3: |
🎯 Key Takeaways
- PHP evaluates if/elseif/else top-to-bottom and stops the moment a condition is true — order your conditions from most specific to most general or you'll get wrong results.
- Always add break to switch cases unless you deliberately want fall-through between adjacent cases sharing the same outcome — missing break is one of the most common silent PHP bugs in production.
- Choose your loop by what you know: for when you know the count upfront, while when you don't, foreach when you have an array. Using the wrong loop type makes your intent harder to read.
- PHP 8's match expression is strictly safer than switch — it uses === comparison, doesn't fall through, and throws a catchable error for unhandled cases instead of silently doing nothing.
- Always use strict comparison (===) in conditionals. Loose comparison (==) triggers PHP's type juggling, which produces surprising results like '' == false being true and '0' == false being true.
- After any foreach that uses
&(reference), immediately callto break the reference. Without it, reusing the variable name in a subsequent loop silently corrupts your array.unset() - The null coalescing operator (??) is the cleanest way to provide safe defaults for missing values. Use it instead of
— it's shorter and more readable.isset()? $value : $default - Use guard clauses (early returns/checks) instead of deeply nested if statements. If you're three levels of nesting deep, your code needs refactoring.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QWhat is the difference between
if,elseif, andelse? Can you have multipleelseifblocks? What about multipleelseblocks? - QWhat is the difference between
switchandif/elseif? When would you choose one over the other? - QWhat happens if you forget a
breakin aswitchstatement? Give an example of when fall-through is intentional and useful. - QExplain the difference between
breakandcontinueinside a loop. Provide a real-world scenario for each. - QWhat is the difference between
whileanddo-while? When would you choosedo-whileoverwhile? - QWhat is the difference between
andisset()in PHP? When would you use each?empty() - QWhat is the 'foreach reference trap'? Why does it happen, and how do you fix it?
- QWhat are the advantages of PHP 8's
matchexpression over the traditionalswitchstatement? - QWhat is the null coalescing operator (
??)? How is it different from the ternary operator (?:)? - QWhy is strict comparison (
===) preferred over loose comparison (==) in conditionals?
Frequently Asked Questions
What is the difference between if/else and switch in PHP?
Use if/else when your conditions involve ranges, multiple variables, or complex logic (like 'is score >= 90'). Use switch when you're comparing one variable against many specific exact values (like checking a status code). Switch is easier to read for many specific cases, but if/else is more flexible for complex conditions. In PHP 8+, prefer match over switch for value-based branching — it's stricter and safer.
Can a PHP if statement run without an else block?
Absolutely — the else block is always optional. You only need else if there's something specific you want to happen when the condition is false. If you just want to 'do something or do nothing', a standalone if is perfectly valid and very common. In fact, experienced developers often use guard clauses — standalone if statements that check for failure conditions and exit early — instead of wrapping everything in an if/else.
What is the difference between break and continue in a PHP loop?
break exits the loop completely — no more iterations happen after it. continue skips only the current iteration and immediately moves to the next one, keeping the loop running. Think of break as 'stop the whole loop' and continue as 'skip this one, keep going.' In practice: use continue to filter items (skip bad ones, process good ones), and use break to find items (stop as soon as you find what you're looking for).
What is the difference between do-while and while loops?
A while loop checks the condition BEFORE running the code — if the condition is false from the start, the code never executes. A do-while loop runs the code FIRST, then checks the condition — so it always executes at least once. Use do-while for menu systems, retry patterns, and input validation where you want to show something to the user before asking if they want to continue. Use while for processing where zero iterations is a valid outcome.
When should I use foreach vs for loops in PHP?
Use foreach whenever you're iterating over an array or collection — it's cleaner, less error-prone, and automatically handles keys and values. Use for when you need a counter, need to iterate a specific number of times, or need to access elements by index (like processing every other element). In PHP development, foreach is used about 80% of the time because most iteration is over arrays, database results, or collections.
What is the difference between isset() and empty() in PHP?
isset() returns true if the variable exists and is not null. empty() returns true if the variable doesn't exist OR if its value is falsy (0, '', '0', false, null, []). The key difference: isset(0) returns true (the variable exists), but empty(0) returns true (0 is falsy). Use isset() when you want to know 'does this variable exist?' Use empty() when you want to know 'does this variable have a meaningful value?'
What is the null coalescing operator (??) and how does it differ from a ternary?
The null coalescing operator $a ?? $b returns $a if it exists and is not null, otherwise $b. A standard ternary $a ? $a : $b returns $a if $a is truthy, otherwise $b. The difference: null coalescing only checks for null/existence, while ternary checks for truthiness. This means 0 ?? 'default' returns 0 (it exists), but 0 ? 0 : 'default' returns 'default' (0 is falsy). Use ?? for safe defaults on optional values. Use ternary for actual true/false decisions.
How do you handle nested if statements without making code unreadable?
Use guard clauses — check for failure conditions first and exit early (return, continue, or break), so the 'happy path' code stays at the top level of indentation. Instead of nesting three if statements, check the opposite conditions and return/exit early. This flattens your code from 3 levels of nesting to 1 level, making it dramatically easier to read and maintain. Most PHP style guides recommend a maximum nesting depth of 2-3 levels.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.