In PHP, the string "1e1" is equal to the string "10", and that can send your switch statement down the wrong path. Here's why, how match fixes it, and three more PHP 8 features worth using.
Every example below was tested on PHP 8.1 and PHP 8.5, and both gave the same results.
Video version:
The trap
function check($v) { switch ($v) { case "10": return "ten"; default: return "other"; } } check("1e1"); // "ten" check("010"); // "ten" check("10.0"); // "ten" check(" 10"); // "ten" check("10abc"); // "other" Four different strings, and all of them are treated as "10".
Why: switch compares with ==
switch uses loose comparison (==). When both strings look like numbers, PHP compares them as numbers:
"1e1" == "10" // true (1e1 is scientific notation: 1 × 10¹ = 10) "1e1" === "10" // false The same rule causes a famous security bug, known as "magic hashes":
md5('240610708') // "0e462097431906509019562988736854" md5('QNKCDZO') // "0e830400451993494058024219903391" md5('240610708') == md5('QNKCDZO') // true 😱 Both hashes look like 0e<digits>, which is "0 × 10^something" = 0. So 0 == 0, and two completely different hashes are "equal". If a token or password-hash check uses ==, that's a real vulnerability. Use ===, or better, hash_equals() for hashes (it's also timing-safe).
PHP 8 did fix the worst one: "abc" == 0 is now false (it was true in PHP 7). But numeric strings are still compared as numbers.
match is strict
// switch switch ($v) { case "10": $label = "ten"; break; default: $label = "other"; } // match (PHP 8.0+) $label = match ($v) { "10" => "ten", default => "other", }; match compares with ===, so "1e1" gives "other". So does the integer 10, because the types differ. It's also an expression: it returns a value, so seven lines become four, with no break.
No fall-through
The other classic switch bug is a missing break:
switch ($code) { case 200: $msg = "OK"; // no break! case 404: $msg = "Not found"; break; } // $code = 200 → $msg is "Not found" ❌ match arms never fall through, and one arm can handle several values:
$msg = match ($code) { 200 => "OK", 404, 410 => "Not found", }; No match? It throws
echo match (500) { 200 => "OK", 404 => "Not found" }; // UnhandledMatchError: Unhandled match case 500 switch silently does nothing when no case matches. match throws, so the bug shows up immediately instead of in production data.
Bonus: match (true) replaces long if/elseif chains:
$age = 25; echo match (true) { $age < 13 => "child", $age < 20 => "teen", default => "adult", }; // "adult" 3 more PHP 8 features to use
Nullsafe operator. You get null instead of an error when something in the chain is null:
$city = $user?->address?->city; Named arguments. Skip optional parameters, use any order, and make the call self-documenting:
str_pad("5", 3, pad_type: STR_PAD_LEFT, pad_string: "0"); // "005" htmlspecialchars($html, double_encode: false); Enums. A fixed set of allowed values, and a perfect partner for match:
enum Status: string { case Active = 'active'; case Banned = 'banned'; public function label(): string { return match ($this) { Status::Active => 'Active ✅', Status::Banned => 'Banned 🚫', }; } } Status::from('active')->label(); // "Active ✅" Status::tryFrom('nope'); // null Status::from('nope'); // ValueError: "nope" is not a valid backing value for enum Status Cheat sheet
-
switchuses==, so"1e1"matches"10" -
matchuses===, returns a value, and has no fall-through - No match means
UnhandledMatchErrorinstead of silence - Compare hashes with
hash_equals(), never== - Also use
?->, named arguments and enums
Are you still using switch, or have you moved to match? 👇
I post one tested coding tip every day on YouTube at Refactorly. The 30-second version of this one is here.



