PHP match Expression
The match expression, introduced in PHP 8.0, is a compact alternative to switch that compares values strictly and returns a result directly. Instead of writing a block of case statements with break, you write a single expression that evaluates to a value. This makes code shorter, safer (no accidental fall-through), and easier to use inside assignments, return statements, and function calls.
Overview / How it works
match takes a subject value, compares it against a list of conditions, and evaluates to the result tied to the first condition that matches. Two things separate it fundamentally from switch:
- It is an expression, not a statement. A
switchblock executes statements; amatchblock produces a value you can assign, return, or pass to a function —$x = match(...) { ... };. - It compares using strict equality (
===), not loose equality (==).switchfamously matches0 == "hello"as true in older PHP versions because of type juggling.matchnever does this: types must be identical, not just “equal after conversion.”
Internally, when the Zend engine compiles a match expression, it evaluates the subject expression exactly once, then walks the arms in source order, testing each condition with an identical-types comparison. As soon as one succeeds, the corresponding result expression is evaluated and returned as the value of the whole match — no other arms are touched, and there is no fall-through between arms (so you never need break). If none of the arms match and there is no default arm, PHP throws an UnhandledMatchError, an unchecked exception you can catch like any other Throwable. This “throw if nothing matches” behavior is intentional: PHP’s designers wanted match to fail loudly on unexpected input rather than silently doing nothing, which is a common source of bugs with switch.
Because match is an expression, its result arms can themselves be arbitrary expressions — including throw expressions, function calls, or other match expressions nested inside. This lets you use match both as a lookup table and as a validation/branching tool.
Syntax
<?php
$result = match (subject_expression) {
condition1, condition2 => result_expression1,
condition3 => result_expression2,
default => result_expressionN,
};
| Part | Description |
|---|---|
subject_expression |
The value being compared. Evaluated exactly once, no matter how many arms exist. |
condition |
One or more comma-separated values compared to the subject using ===. Any of them matching triggers that arm. |
=> |
Separates the condition list from the result expression for that arm. |
result_expression |
The value produced when the arm matches. Can be any expression: a literal, a function call, a throw, another match, etc. |
default |
Optional catch-all arm. If omitted and nothing matches, PHP throws UnhandledMatchError. |
A trailing comma after the last arm is allowed and is common style, since it makes future additions produce cleaner diffs.
Examples
Example 1: Basic value lookup
<?php
$fruit = "apple";
$price = match ($fruit) {
"apple", "pear" => 0.99,
"banana" => 0.59,
"cherry" => 3.49,
default => throw new ValueError("Unknown fruit: $fruit"),
};
echo "Price: \${$price}\n";
Output:
Price: $0.99
Here "apple", "pear" share one arm, so either value produces 0.99. The default arm doesn’t return a value at all — it throws an exception, which is perfectly valid because throw is itself an expression in PHP. This turns an “unexpected fruit” bug into a loud, immediate failure instead of a silently wrong price.
Example 2: match(true) for range/condition checks
<?php
function letterGrade(int $score): string
{
return match (true) {
$score >= 90 => 'A',
$score >= 80 => 'B',
$score >= 70 => 'C',
$score >= 60 => 'D',
default => 'F',
};
}
foreach ([95, 82, 58] as $score) {
echo "$score => " . letterGrade($score) . "\n";
}
Output:
95 => A
82 => B
58 => F
When the subject is the literal true, each “condition” becomes a boolean expression evaluated against true. The first arm whose expression is itself true wins. This is the idiomatic way to express ranges or multi-condition logic with match, since a plain match ($score) can only test exact values, not ranges.
Example 3: match with enums
<?php
enum Status
{
case Draft;
case Published;
case Archived;
}
function label(Status $status): string
{
return match ($status) {
Status::Draft => 'Draft',
Status::Published => 'Live',
Status::Archived => 'Archived',
};
}
foreach (Status::cases() as $status) {
echo label($status) . "\n";
}
Output:
Draft
Live
Archived
Because every Status case is handled explicitly and enum cases are a closed, known set, this match needs no default arm. If a new case is later added to the enum without updating this function, PHP will throw UnhandledMatchError at runtime the moment that case reaches this code — a useful safety net that flags incomplete refactors instead of hiding them.
How it works step by step / Under the hood
Consider this expression, which highlights the strict-comparison behavior directly:
<?php
var_dump(match ("1") {
1 => "int one",
"1" => "string one",
default => "no match",
});
Output:
string(10) "string one"
Step by step, PHP does the following:
- Evaluates the subject expression,
"1"(a string), exactly once and holds it in a temporary. - Tests the first arm’s condition,
1(an integer), using===. Since a string is never===to an integer regardless of value, this arm is skipped. - Tests the second arm’s condition,
"1"(a string). Type and value both match, so this arm is selected. - Evaluates and returns that arm’s result expression,
"string one", without looking at any remaining arms — includingdefault.
With a switch statement, the same subject would loosely match the integer case first (because "1" == 1 is true under loose comparison), producing a different, often surprising result. This strictness is the single biggest behavioral difference to internalize when migrating from switch to match.
Common Mistakes
Mistake 1: Omitting default and assuming every case is covered
<?php
$level = "info";
$color = match ($level) {
"error" => "red",
"warning" => "yellow",
};
echo $color;
This looks harmless, but $level can be "info", "debug", or any other string a caller passes in. Since none of those match "error" or "warning" and there is no default arm, PHP throws UnhandledMatchError: Unhandled match case 'info' at runtime instead of quietly returning null the way an unmatched switch would. The fix is to always add a default arm (or be certain, as with an exhaustive enum, that every possible value truly is listed):
<?php
$level = "info";
$color = match ($level) {
"error" => "red",
"warning" => "yellow",
default => "gray",
};
echo $color;
Output:
gray
Mistake 2: Expecting loose comparison like switch
<?php
$value = "0";
$result = match ($value) {
0 => "zero (int)",
false => "falsy",
default => "no match",
};
echo $result;
Output:
no match
A developer used to switch might expect the string "0" to match either 0 or false, since PHP treats "0" as falsy in boolean contexts and "0" == 0 is true under loose comparison. But match uses ===, so a string never equals an int or a bool, and execution falls through to default. When porting switch logic to match, always double-check that the types being compared, not just the values, actually line up — cast explicitly (e.g. (int) $value) if you need numeric comparison.
Best Practices
- Prefer
matchoverswitchwhenever you’re producing a value — it’s shorter, has no fall-through bugs, and its strict comparison catches type mistakes earlier. - Always include a
defaultarm unless the subject’s possible values are a closed, exhaustive set (like every case of a specific enum) and you deliberately want anUnhandledMatchErrorfor anything else. - Use
match (true) { ... }for range checks and compound boolean conditions; it reads more clearly than a chain ofif/elseifwhen every branch just returns a value. - Group conditions that share an outcome with a comma (
"apple", "pear" => ...) instead of duplicating arms. - Remember the strict-comparison rule when migrating
switchstatements — audit each case for type mismatches before assuming behavior is identical. - Let
matchthrow inside adefaultarm (default => throw new ...) when an unexpected value indicates a real bug rather than a value you want to silently ignore.
Practice Exercises
- Write a function
httpStatusText(int $code): stringthat usesmatchto map common HTTP status codes (200, 301, 404, 500) to their text descriptions, with adefaultarm returning"Unknown Status". - Rewrite this
switchstatement as amatchexpression and explain, in a comment, what behavior changes because of strict comparison:switch ($input) { case 0: $r = "zero"; break; case "0": $r = "string zero"; break; default: $r = "other"; } - Using
match (true), write a functioncategorizeAge(int $age): stringthat returns"child"for ages under 13,"teen"for 13–19, and"adult"for 20 and above. Test it with ages 8, 15, and 40 and predict the output before running it.
Summary
matchis an expression that returns a value, unlike the statement-basedswitch.- Comparisons in
matchuse strict equality (===), so types must match exactly — no implicit type juggling. - There is no fall-through between arms and no
breakkeyword needed; the first matching arm’s result is returned immediately. - If no arm matches and there’s no
default, PHP throws anUnhandledMatchError. - Arms can share conditions via commas, and result expressions can be any expression, including
throw. match (true) { ... }is the idiomatic pattern for range and compound-condition checks.
