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.
Top comments (0)