One value, many possibilities
When you compare the same variable against a list of values, a long if / elseif chain repeats itself:
<?php
if ($status === 'pending') {
$label = 'Waiting for payment';
} elseif ($status === 'paid') {
$label = 'Preparing your order';
} elseif ($status === 'shipped') {
$label = 'On its way';
} // ...and so onPHP has two constructs for this situation: the classic switch statement and the newer match expression from PHP 8.
switch
A switch takes one value and jumps to the case that equals it. Each case ends with break, which leaves the switch. default runs when no case matches:
<?php
$status = "shipped";
switch ($status) {
case "pending":
echo "Waiting for payment\n";
break;
case "paid":
echo "Preparing your order\n";
break;
case "shipped":
echo "On its way\n";
break;
default:
echo "Unknown status\n";
}On its wayFall-through
The break is easy to forget, and forgetting it changes the meaning. Without break, PHP keeps going into the next case, whatever its label:
<?php
$status = "paid";
switch ($status) {
case "paid":
echo "Preparing your order\n";
case "shipped":
echo "On its way\n";
break;
default:
echo "Unknown status\n";
}Preparing your order
On its wayThat is usually a bug. Sometimes it is used on purpose to give several values the same code, by stacking empty cases:
<?php
$day = "Sun";
switch ($day) {
case "Sat":
case "Sun":
echo "Weekend: open 10 to 4\n";
break;
default:
echo "Weekday: open 9 to 6\n";
}Weekend: open 10 to 4switch compares loosely
switch uses ==, so the type juggling described in Comparisons, == versus === and type juggling applies:
<?php
$quantity = "1";
switch ($quantity) {
case 1:
echo "Matched the integer 1\n";
break;
default:
echo "No match\n";
}Matched the integer 1match
PHP 8 introduced match, which fixes switch's three weak spots. It is an expression, so it returns a value; it compares with ===; and it never falls through.
<?php
$status = "shipped";
$label = match ($status) {
"pending" => "Waiting for payment",
"paid" => "Preparing your order",
"shipped" => "On its way",
"delivered" => "Delivered",
default => "Unknown status",
};
echo $label . "\n";On its wayEach arm is value => result, separated by commas. Several values can share an arm:
<?php
function openingHours(string $day): string
{
return match ($day) {
"Sat", "Sun" => "10 to 4",
"Mon", "Tue", "Wed", "Thu", "Fri" => "9 to 6",
};
}
echo openingHours("Sun") . "\n";
echo openingHours("Wed") . "\n";10 to 4
9 to 6No match is an error
match must produce a value. If nothing matches and there is no default arm, it throws an UnhandledMatchError. That sounds harsh, but it turns a silent bug (an unexpected value slipping through) into an error you notice:
<?php
$day = "Holiday";
echo match ($day) {
"Sat", "Sun" => "Weekend",
"Mon", "Tue", "Wed", "Thu", "Fri" => "Weekday",
};Fatal error: Uncaught UnhandledMatchError: Unhandled match case 'Holiday' in /path/to/script.php:4
Stack trace:
#0 {main}
thrown in /path/to/script.php on line 4Strict comparison
Because match uses ===, the string "1" does not match the integer 1:
<?php
$quantity = "1";
echo match ($quantity) {
1 => "integer one",
"1" => "string one",
} . "\n";string onematch(true) for ranges
match compares values, but you can test a list of conditions by matching against true. The first arm whose condition is true wins:
<?php
function band(int $score): string
{
return match (true) {
$score >= 90 => "A",
$score >= 75 => "B",
$score >= 50 => "C",
default => "Fail",
};
}
echo band(82) . " " . band(91) . " " . band(30) . "\n";B A FailWhich one to use
Prefer match when you are turning one value into another, which covers most cases: labels, prices per plan, CSS classes per status. Use switch (or plain if) when each case needs several statements, because a match arm holds a single expression. For a fixed set of statuses, enums combine nicely with match.
A useful test: if you are about to write $label = ... in every case of a switch, rewrite it as one match assigned to $label. The result is shorter, every arm is visibly a value, and an unexpected input can no longer slip through unnoticed, because it either reaches default or throws an error you will see straight away.
Common mistakes
- Forgetting break in a switch, so the next case runs too.
- Expecting switch to compare strictly. It uses
==. - Leaving out default in a match when unexpected values are possible, which throws an error.
- Trying to put several statements in a match arm. Call a function from the arm instead.
Exercise
Write a function planPrice(string $plan): int with match that returns 0 for "free", 499 for "pro" and 1999 for "team", and throws an error for anything else. Then print the price of "pro" and "team".
Show one possible answer
<?php
function planPrice(string $plan): int
{
return match ($plan) {
"free" => 0,
"pro" => 499,
"team" => 1999,
};
}
echo planPrice("pro") . "\n";
echo planPrice("team") . "\n";499
1999Leaving out default is deliberate here: an unknown plan name should fail loudly rather than quietly cost nothing.
Key points
- switch compares one value against several case labels with ==, and needs break to stop falling through.
- match is an expression that returns a value, compares with ===, and never falls through.
- match throws an UnhandledMatchError when no arm matches and there is no default.
- match(true) is a tidy way to test a list of conditions in order.