skip to content

Branching & Loops

PHP offers switch with loose comparison and fall-through, and since 8.0 the strict, exhaustive match expression. Interviewers probe that choice and the foreach-by-reference trap.

part ofPHPoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In PHP 8, what are the differences between a switch statement and a match expression?

level: juniorimportance: must knowfreq 74%

answer

  1. statement vs expression that returns
  2. == vs ===
  3. fall-through needs break; match never falls
  4. no arm matched: UnhandledMatchError
  5. comma-separated conditions, single expression per arm

basics

~20 s

switch is a statement that compares loosely (==) and falls through until a break. match, since PHP 8.0, is an expression that compares strictly (===), never falls through, returns a value and throws UnhandledMatchError when nothing matches.

solid answer

~40 s

`switch` is a **statement**: it compares its subject with each `case` using **loose `==`**, runs from the first matching case onward and keeps **falling through** into later cases until a `break`. With no matching case and no `default`, it silently does nothing. `match`, available since **PHP 8.0**, is an **expression** that evaluates to a value, compares with **strict `===`**, runs exactly one arm with no fall-through, and throws **`UnhandledMatchError`** when nothing matches and there is no `default`. Each `match` arm is a single expression, several conditions share one arm with commas (`2, 3 => 'In transit'`), and conditions are evaluated lazily, top to bottom. Use `match` to map a value to a result, such as a parcel status code to a label; keep `switch` for running several statements per case.

code

php · 25 lines
php
<?php
declare(strict_types=1);

$statusCode = 3;

switch ($statusCode) {            // loose ==, statement
    case 1:
        $label = 'Label created';
        break;
    case 2:
    case 3:                       // 2 and 3 share a body by falling through
        $label = 'In transit';
        break;
    default:
        $label = 'Unknown';
}

$label = match ($statusCode) {    // strict ===, expression
    1 => 'Label created',
    2, 3 => 'In transit',
    4 => 'Delivered',
    default => 'Unknown',
};

echo $label, PHP_EOL;             // In transit

go deeper

for a junior

Know that match returns a value, compares strictly with ===, needs no break and throws UnhandledMatchError when nothing matches.

for a middle

Explain fall-through in switch, lazy top-to-bottom evaluation of match conditions, comma-separated conditions and the match (true) idiom for ranges.

for a senior

Judge where the stricter comparison and exhaustiveness of match catch real bugs, such as unmapped status codes, and where input types must be normalised first.

for a principal

Set a codebase convention on when to use match, switch or polymorphism for branching on codes, weighing exhaustiveness errors against silent defaults.

## Two constructs for the same shape of problem Both `switch` and `match` take a **subject** value and compare it against a list of alternatives. That is where the similarity ends. `switch` is PHP's classic C-style statement; `match`, added in **PHP 8.0**, was designed to remove the traps of `switch` for the common case of "turn this value into that value". ## Side-by-side | Aspect | `switch` | `match` | |---|---|---| | Kind | statement | expression, evaluates to a value | | Comparison | loose `==` | strict `===` | | After a match | falls through to later cases until `break` | runs one arm only | | Nothing matches, no `default` | does nothing | throws `UnhandledMatchError` | | Arm body | any number of statements | a single expression | | Several values, one body | stacked `case 2: case 3:` | `2, 3 =>` | | Available since | always | PHP 8.0 | ## Loose versus strict comparison `switch` compares with `==`, so PHP's type-juggling rules apply. A string `"1"` from a query parameter matches `case 1:`; two numeric strings compare as numbers, so `"1"` even matches `case "01":` first if that case comes earlier. `match` compares with `===`: `"1"` does not match the arm `1 =>`. That makes `match` predictable, and it also means the types of your data must be right before you match. ## Fall-through In a `switch`, execution starts at the matching `case` and continues through every following `case` body until it hits `break` (or `return`, `continue`, `throw`). Forgetting a `break` is a classic bug: status 1 prints "Label created" *and* "In transit". Fall-through is sometimes deliberate, to share one body between cases, but it must be read carefully. `match` has no fall-through at all; shared bodies are written with commas. ## Exhaustiveness A `match` must handle its subject. If no condition is identical to the subject and there is no `default` arm, PHP throws **`UnhandledMatchError`**, a subclass of `Error`, with a message such as `Unhandled match case 5`. A `switch` without a `default` simply skips everything. The error is a feature: a new parcel status that nobody mapped fails loudly instead of producing an empty label. ## Expression semantics Because `match` is an expression, it is assigned or returned directly: ```php $label = match ($statusCode) { 1 => 'Label created', 2, 3 => 'In transit', 4 => 'Delivered', default => 'Unknown', }; ``` A few rules follow from being an expression: - Each arm holds **one expression**. For several steps, call a function, or use a `throw` expression (PHP 8.0 made `throw` an expression) to fail in an arm. - Conditions are evaluated **in order and lazily**: a condition is only evaluated if every earlier one failed, and only the winning arm's result is evaluated. - A `match` used as a statement on its own still needs a terminating semicolon. - Two `default` arms are a compile error. ## Ranges and other non-identity checks For conditions that are not "equals this value", the idiom is `match (true)` with boolean conditions: ```php $band = match (true) { $weightKg < 2 => 'small', $weightKg < 10 => 'medium', default => 'large', }; ``` The first arm whose condition is identical to `true` wins. With `switch (true)` the same trick works but compares loosely, so a truthy non-boolean case expression would also match. ## Choosing between them 1. Mapping a value to a value, such as a status code to a label: **`match`**. 2. Several statements per branch, or deliberate shared fall-through: **`switch`**, or `if`/`elseif`. 3. Input whose type is uncertain (strings from a request or a database): cast it first, then `match`; do not rely on `switch`'s loose comparison to paper over types. A PHP 8.5 detail on the old form: ending a `case` label with a semicolon (`case 1;`) instead of a colon was always accepted and is now deprecated.

  • What happens in PHP when a switch case has no break?
    Execution falls through: after the matching case's statements, PHP keeps running the next case bodies, without testing their labels, until it reaches a `break`, `return`, `continue`, `throw` or the end of the switch. That is useful for deliberately sharing a body, but an accidental missing `break` runs code meant for other values.
  • Can a match arm run several statements?
    Not directly: an arm's right-hand side is a single expression. Put the steps in a function or method and call it from the arm, or use an immediately invoked closure. To fail inside an arm, use a `throw` expression, which PHP 8.0 made legal in expression position.
  • How do you express a range check such as weight below 2 kg with match?
    Use `match (true)` and write boolean conditions as the arms: `$weight < 2 => 'small', $weight < 10 => 'medium', default => 'large'`. The first condition identical to `true` wins. Because the comparison is strict, a condition must evaluate to real `true`, not just a truthy value.

saying these in an interview costs you the question

  • Says match compares with == just like switch does.
  • Believes a switch without a default throws an error when nothing matches.
  • Claims match arms fall through to the next arm without a break.
  • Thinks match is available in PHP 7.4.
  • Says a match expression without default returns null when no arm matches.
open as a page

In PHP, why does a foreach by value that reuses the variable of an earlier foreach by reference corrupt the last element?

level: middleimportance: must knowfreq 56%

basics

~20 s

After foreach ($arr as &$value), $value remains a reference to the last element. A later by-value foreach reusing $value writes each element into that slot, leaving it equal to the second-to-last value. unset($value) after the first loop prevents it.

open as a page

In PHP, how do break N and continue N work in nested loops, and why does continue inside a switch behave like break?

level: middleimportance: should knowfreq 26%

basics

~20 s

break N leaves N enclosing loops or switches; continue N jumps to the next iteration of the Nth one. switch counts as a level, so a plain continue inside it acts like break, and PHP warns you to use continue 2.

open as a page

In PHP 7 and later, what happens when you add or change array elements inside a foreach loop over that same array?

level: middleimportance: should knowfreq 32%

basics

~20 s

By value, foreach iterates the array as it stood when the loop began, so changes made in the body are not visited. By reference, it follows the live array and visits appended elements. Since PHP 7.0 foreach never moves the internal pointer.

open as a page

After replacing a switch with match in a PHP 8 parcel tracker, status codes read from the database start throwing UnhandledMatchError. Why, and how do you fix it safely?

level: seniorimportance: should knowfreq 30%

basics

~20 s

switch compared the database string '2' loosely with case 2; match uses ===, so '2' matches no int arm and throws UnhandledMatchError. Validate and cast at the boundary, then decide deliberately how unknown codes should fail.

open as a page