PHP Match
What Is the match Expression? In simple terms: match (PHP 8.0+) checks a value against a list of possibilities and returns the result for the first one that matches. It is a safer, shorter alternative
What Is the match Expression?
In simple terms: match (PHP 8.0+) checks a value against a list of possibilities and returns the result for the first one that matches. It is a safer, shorter alternative to switch for choosing a value.
Technically: match is an expression. It evaluates a subject expression, compares it with each arm's conditions using strict comparison (===), and returns the result of the first matching arm. There is no fall-through, and if no arm matches, PHP throws an UnhandledMatchError.
Why match Was Added
switch has three well-known weaknesses:
- Loose comparison (
==) causes unexpected matches, such as'0'matchingfalse. - Fall-through: a forgotten
breaksilently runs the next case. - It is a statement, so producing a value requires assigning a variable in every case.
match fixes all three.
Syntax
$result = match (subjectExpression) {
condition1 => resultExpression1,
condition2, condition3 => resultExpression2,
default => defaultResultExpression,
};
| Part | Meaning |
|---|---|
match (subjectExpression) | The value to test, evaluated once |
condition => result | An arm: if the subject is identical (===) to the condition, the result is returned |
condition2, condition3 => | Several conditions sharing one result, separated by commas |
default => | Result when no other arm matches (optional) |
; | match is an expression, so the statement that uses it ends with a semicolon |
Example
<?php
$status = 'shipped';
$label = match ($status) {
'pending' => 'Awaiting payment',
'paid', 'processing' => 'Preparing your order',
'shipped' => 'On the way',
'delivered' => 'Delivered',
default => 'Unknown status',
};
echo $label;
Output
On the way
How It Works
- The subject
$statusis evaluated once:'shipped'. - PHP compares it with each arm's conditions from top to bottom using
===. - The arm
'shipped' => 'On the way'matches, so its result is returned and assigned to$label. - No
breakis needed —matchnever falls through to other arms. 'paid', 'processing'shows how several values share one result.
Strict Comparison
match compares with ===, so both value and type must match.
Example: match vs switch
<?php
$value = '1';
$withMatch = match ($value) {
1 => 'integer one',
'1' => 'string one',
};
switch ($value) {
case 1:
$withSwitch = 'integer one';
break;
case '1':
$withSwitch = 'string one';
break;
}
echo "match: $withMatch\n";
echo "switch: $withSwitch\n";
Output
match: string one
switch: integer one
switch uses ==, so the string '1' matches the integer case 1 first. match skips it because the types differ and finds the correct arm.
Consequence for Input Data
Values from forms and URLs are strings. If your arms use integers, convert the input first:
<?php
$pageInput = '2'; // from $_GET
$title = match ((int) $pageInput) {
1 => 'Introduction',
2 => 'Installation',
3 => 'Syntax',
default => 'Lesson not found',
};
echo $title;
Output
Installation
Without the (int) cast, '2' would not be identical to 2, and the result would be 'Lesson not found'.
No Match: UnhandledMatchError
If no arm matches and there is no default, match throws an UnhandledMatchError:
<?php
$status = 'archived';
try {
$label = match ($status) {
'draft' => 'Draft',
'published' => 'Published',
};
} catch (\UnhandledMatchError $e) {
echo get_class($e) . " caught: no arm handles '$status'\n";
}
Output
UnhandledMatchError caught: no arm handles 'archived'
The error message itself includes the unhandled value (its exact wording differs slightly between PHP versions).
Why Throwing Is Good
With switch, an unexpected value silently does nothing, and the bug appears later — for example, as an undefined variable. match fails immediately and loudly at the place where the unexpected value appeared. Add a default arm only when there is a sensible fallback; otherwise, let match throw so that missing cases are discovered during development.
Throwing a Custom Exception
Since PHP 8.0, throw is also an expression, so it can be used as an arm's result:
<?php
function boxPrice(string $size): int
{
return match ($size) {
'S' => 49,
'M' => 79,
'L' => 119,
default => throw new InvalidArgumentException("Unknown box size: $size"),
};
}
echo boxPrice('M') . "\n";
try {
echo boxPrice('XL') . "\n";
} catch (InvalidArgumentException $e) {
echo "Error: " . $e->getMessage() . "\n";
}
Output
79
Error: Unknown box size: XL
A descriptive exception is easier to understand in logs than a generic UnhandledMatchError.
Only the Matching Arm Is Evaluated
Arm results are evaluated lazily: only the result of the matching arm runs.
<?php
function loadAdminDashboard(): string
{
echo "(loading admin data...)\n";
return 'Admin dashboard';
}
function loadUserDashboard(): string
{
echo "(loading user data...)\n";
return 'User dashboard';
}
$role = 'user';
$page = match ($role) {
'admin' => loadAdminDashboard(),
'user' => loadUserDashboard(),
default => 'Guest page',
};
echo $page . "\n";
Output
(loading user data...)
User dashboard
loadAdminDashboard() is never called. This makes it safe to put function calls in arms.
match (true): Matching Conditions
When the subject is true, each arm's condition can be any boolean expression. The first condition that is true wins. This is the standard way to handle ranges:
<?php
function grade(int $score): string
{
return match (true) {
$score >= 90 => 'A',
$score >= 75 => 'B',
$score >= 60 => 'C',
default => 'F',
};
}
foreach ([95, 82, 67, 45] as $score) {
echo "$score: " . grade($score) . "\n";
}
Output
95: A
82: B
67: C
45: F
How It Works
- Each condition (for example
$score >= 75) evaluates totrueorfalse. matchcompares that result with the subjecttrueusing===.- Arms are checked in order, so they must go from the highest threshold to the lowest — the same rule as an
if...elseifchain.
Note: Conditions in
match (true)must produce real booleans. A condition such as$name(a non-empty string) is truthy but not identical totrue, so it would not match. Use explicit comparisons like$name !== ''.
Practical Example: HTTP Status Messages
<?php
function statusText(int $code): string
{
return match ($code) {
200 => 'OK',
201 => 'Created',
301, 302 => 'Redirect',
400 => 'Bad Request',
401, 403 => 'Access Denied',
404 => 'Not Found',
500 => 'Server Error',
default => 'Unknown status',
};
}
foreach ([200, 302, 403, 404, 418] as $code) {
echo "$code: " . statusText($code) . "\n";
}
Output
200: OK
302: Redirect
403: Access Denied
404: Not Found
418: Unknown status
Grouping codes with commas (301, 302) keeps related values together without repeating the result.
Practical Example: Subscription Pricing
<?php
function subscriptionPrice(string $plan, string $cycle): int
{
$monthly = match ($plan) {
'basic' => 299,
'pro' => 799,
'business' => 1999,
default => throw new InvalidArgumentException("Unknown plan: $plan"),
};
return match ($cycle) {
'monthly' => $monthly,
'yearly' => (int) round($monthly * 12 * 0.8), // 20% yearly discount
default => throw new InvalidArgumentException("Unknown billing cycle: $cycle"),
};
}
$requests = [['basic', 'monthly'], ['pro', 'yearly'], ['business', 'yearly'], ['enterprise', 'monthly']];
foreach ($requests as [$plan, $cycle]) {
try {
echo "$plan ($cycle): ₹" . number_format(subscriptionPrice($plan, $cycle)) . "\n";
} catch (InvalidArgumentException $e) {
echo "Error: " . $e->getMessage() . "\n";
}
}
Output
basic (monthly): ₹299
pro (yearly): ₹7,670
business (yearly): ₹19,190
Error: Unknown plan: enterprise
Step-by-Step Explanation
- The first
matchmaps the plan name to a monthly price, or throws for an unknown plan. - The second
matchcalculates the final price for the billing cycle. The yearly price is 12 months with a 20% discount:799 × 12 × 0.8 = 7670.4, rounded to7670. (int) round(...)converts the rounded float to an integer.- Each
matchhas a single responsibility, which keeps the function easy to extend: adding a plan means adding one arm.
match vs switch
| Feature | switch | match |
|---|---|---|
| Type | Statement | Expression (returns a value) |
| Comparison | Loose (==) | Strict (===) |
| Fall-through | Yes, without break | Never |
| Multiple values per branch | Stacked case labels | Comma-separated conditions |
| Branch body | Any number of statements | One expression |
| No matching branch | Does nothing (unless default) | Throws UnhandledMatchError |
| Available since | Early PHP | PHP 8.0 |
When to Use Which
- Use
matchto choose or calculate a value from a single input — the most common case. - Use
switchorif...elseifwhen a branch must execute several statements (or call a function that does the work and return its result from amatcharm). - Use
match (true)for range checks that return a value.
Common Mistakes
Forgetting the Semicolon
Effect: Parse error. match is part of an assignment or return statement, which needs a closing ; after the }.
Fix: $x = match (...) { ... };
Comparing Strings With Integer Arms
Effect: Input such as '2' does not match 2, leading to the default arm or an UnhandledMatchError.
Fix: Cast or validate input before matching.
Writing Statements Inside Arms
Effect: Parse error — arms accept a single expression, not blocks of statements.
Fix: Move multi-step logic into a function and call it from the arm, or use switch / if.
Using Non-Boolean Conditions in match (true)
Effect: Truthy values (non-empty strings, non-zero numbers) do not match true.
Fix: Use explicit comparisons that return booleans.
Adding default Too Eagerly
Effect: Unexpected values are hidden behind a generic fallback.
Fix: Use default only when the fallback is genuinely correct; otherwise, let the error surface or throw a descriptive exception.
Best Practices
- Prefer
matchoverswitchfor value selection in PHP 8 code. - Normalize input types before matching, because comparison is strict.
- Group values that share a result with commas.
- Use
match (true)for ordered range checks, from the most specific condition to the least. - Throw descriptive exceptions in
defaultarms for invalid input.
Official reference: https://www.php.net/manual/en/control-structures.match.php