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 switch block executes statements; a match block produces a value you can assign, return, or pass to a function — $x = match(...) { ... };.
  • It compares using strict equality (===), not loose equality (==). switch famously matches 0 == "hello" as true in older PHP versions because of type juggling. match never 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 — including default.

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 match over switch whenever you’re producing a value — it’s shorter, has no fall-through bugs, and its strict comparison catches type mistakes earlier.
  • Always include a default arm unless the subject’s possible values are a closed, exhaustive set (like every case of a specific enum) and you deliberately want an UnhandledMatchError for anything else.
  • Use match (true) { ... } for range checks and compound boolean conditions; it reads more clearly than a chain of if/elseif when 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 switch statements — audit each case for type mismatches before assuming behavior is identical.
  • Let match throw inside a default arm (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): string that uses match to map common HTTP status codes (200, 301, 404, 500) to their text descriptions, with a default arm returning "Unknown Status".
  • Rewrite this switch statement as a match expression 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 function categorizeAge(int $age): string that 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

  • match is an expression that returns a value, unlike the statement-based switch.
  • Comparisons in match use strict equality (===), so types must match exactly — no implicit type juggling.
  • There is no fall-through between arms and no break keyword needed; the first matching arm’s result is returned immediately.
  • If no arm matches and there’s no default, PHP throws an UnhandledMatchError.
  • 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.