switch Statement
A switch statement takes a single value and compares it against a list of possible matches, running the block of code attached to whichever case matches first. It exists to replace long chains of if / elseif / elseif ... that all test the same variable — once you have four or five branches checking the same thing, switch reads more clearly than a stack of elseifs. Under the hood it is not magic: PHP walks the case labels from top to bottom and stops at the first one that matches, so a switch is really just sugar over sequential comparisons with a couple of important quirks you need to know about.
Basic syntax
A switch block opens with the value being tested, followed by one or more case labels and an optional default label for anything that does not match. Each case should normally end with a break so execution does not continue into the next case.
A simple switch
<?php
$day = "Tuesday";
switch ($day) {
case "Monday":
echo "Start of the work week.";
break;
case "Tuesday":
echo "Second day in.";
break;
case "Saturday":
case "Sunday":
echo "Weekend!";
break;
default:
echo "Just another day.";
break;
}Second day in.
Notice "Saturday" and "Sunday" share the same block above — that stacked-case pattern is covered in detail further down.
Why break matters: fall-through
If you omit break, PHP does not stop after running the matched case's code — it keeps executing every statement in the cases below it, regardless of whether they match, until it hits a break or reaches the end of the switch block. This is called fall-through, and it is one of the most common sources of bugs in switch statements written by developers coming from languages that auto-break (or by anyone who simply forgot the keyword).
Accidental fall-through (a bug)
<?php
$level = "warning";
switch ($level) {
case "error":
echo "Critical! ";
// no break here
case "warning":
echo "Please review. ";
// no break here either
case "info":
echo "Logged.";
break;
}Please review. Logged.
Because $level matched "warning", execution starts there — but without a break, it also runs the "info" block. In this example that happens to produce readable output, but imagine the same pattern applied to something like applying a discount or setting a permission level: silently running every case below the match is rarely what you want.
Intentional fall-through
Sometimes you want several cases to run the same trailing logic — for example, treating multiple related statuses as needing the same follow-up action after their own specific handling. Intentional fall-through is fine as long as it is obvious and, ideally, commented so the next reader knows it was not a mistake.
Fall-through used on purpose
<?php
$status = "shipped";
switch ($status) {
case "shipped":
echo "Package is on its way. ";
// fall through intentionally: shipped and delivered both notify
case "delivered":
echo "Customer will be notified.";
break;
case "cancelled":
echo "Order was cancelled.";
break;
}Package is on its way. Customer will be notified.
Grouping cases (stacked case labels)
When several distinct values should all run the exact same block, you can "stack" multiple case labels with no code between them. Only the last label in the stack needs the actual logic and its break — the labels above it just fall through to it, which in this specific pattern is completely intentional and idiomatic.
Grouping weekend days
<?php
function describeDay(string $day): string
{
switch ($day) {
case "Saturday":
case "Sunday":
return "Weekend";
case "Monday":
case "Tuesday":
case "Wednesday":
case "Thursday":
case "Friday":
return "Weekday";
default:
return "Unknown day";
}
}
echo describeDay("Sunday") . "\n";
echo describeDay("Wednesday") . "\n";
echo describeDay("Someday") . "\n";Weekend Weekday Unknown day
Using return inside a function's switch cases (as above) is a clean alternative to break — it exits the function immediately, so there is no risk of accidental fall-through at all.
Loose comparison: the real gotcha
This is the quirk that catches almost everyone at least once: switch compares the switched value against each case using loose comparison (==), the same rules PHP uses for == everywhere else. That means type juggling applies, and values that look completely different can be treated as equal.
A string matching an unexpected numeric case
<?php
$input = "0 items";
switch ($input) {
case 0:
echo "Matched the number zero!";
break;
default:
echo "No match.";
break;
}Matched the number zero!
That output is almost certainly not what you expect from a string like "0 items". What happens is that PHP converts "0 items" to a number for the comparison against case 0, and the leading numeric portion of the string (0) makes the comparison true under == rules. The exact conversion behavior has shifted across PHP versions — PHP 8 tightened several string-to-number comparison rules compared to PHP 7 — but the underlying danger is the same: any loose comparison between a string and a number can produce surprising matches, and switch inherits that danger silently because there is no visual == in the code to remind you.
Confirming it with var_dump
<?php
var_dump("0 items" == 0); // does this string equal the integer 0?
var_dump("abc" == 0); // PHP 8: false (fixed from PHP 7's surprising true)
var_dump("0" == 0); // true - "0" is numeric
var_dump("" == 0); // false in PHP 8 for a plain switch/== comparison of empty string vs 0bool(true) bool(false) bool(true) bool(false)
The safe, reliable fix is to make the type explicit before the switch runs, either by casting the value or by switching on a clearly-typed expression, and to keep your case values the same type as whatever you are testing.
Avoiding the loose-comparison trap
<?php
$input = "0 items";
$numericPart = (int) $input; // 0, but now the intent is explicit
switch (true) {
case $input === "0":
echo "Exactly the string zero.";
break;
case $numericPart === 0 && $input !== "0":
echo "Starts with zero but isn't the string \"0\".";
break;
default:
echo "Something else entirely.";
break;
}Starts with zero but isn't the string "0".
That last example also shows a useful trick: switch (true) lets every case be a full boolean expression evaluated with strict comparison (===), which sidesteps the loose-comparison problem entirely at the cost of losing the simple "match a single value" readability that switch is best at.
default doesn't have to be last
Conventionally default is written as the final label, but PHP does not require that. Wherever it appears, default only runs when nothing above it matched — and just like any other case, it will fall through into whatever comes after it if it lacks a break. Placing it anywhere other than last is legal but tends to confuse readers, so it is best treated as a style rule: always put default last.
switchevaluates the tested expression once, then compares it top-to-bottom against eachcaseusing==.Every
caseshould end inbreak(orreturn/continue/throw) unless fall-through is intentional and commented.Stacked
caselabels with no code between them are the idiomatic way to group values that share one block.switch (true)combined with booleancaseexpressions is a common workaround for the loose-comparison pitfall.