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

  • switch uses ==, so "1e1" matches "10"
  • match uses ===, returns a value, and has no fall-through
  • No match means UnhandledMatchError instead 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.