Match Expression: Converting Switch Statements in PHP 8.0
The switch statement has been part of PHP since version 4.0, released in May 2000. For over two decades, it has served as the primary mechanism for handling multiple conditional branches in PHP applications. During that time, developers have built millions—perhaps billions—of switch statements. Yet this ubiquitous construct carries with it several long-standing design limitations that have caused subtle bugs in production systems: type juggling issues where "1" matches 1, fall-through bugs when break statements are forgotten, and silent failures when cases are omitted.
If you’re maintaining a PHP codebase of any significant age, you likely have hundreds of switch statements scattered throughout your code. Each one represents a decision point—a branch your application can take based on input values. And each one potentially harbors these latent issues. PHP 8.0’s match expression, introduced in November 2020, gives us a new tool to address these problems with greater type safety and clarity.
Before we dive into the specifics of match, let’s take a step back and examine the broader context of conditional logic in PHP. Understanding the historical evolution and the problems that motivated the introduction of match will help us make informed decisions about when and how to use it.
The switch statement has served as PHP’s primary multi-branch conditional mechanism since version 4.0 in 2000. Over the past twenty-plus years, it has proven useful in countless applications. Yet, as developers have gained experience with it, several recurring pain points have emerged. The match expression, introduced in PHP 8.0, represents an effort to address these limitations while maintaining backward compatibility with existing code.
One commonly encountered issue is type juggling in switch statements. Consider a scenario where user input arrives as a string "1" but your cases use integers. Due to loose comparison (==), switch would treat these as equivalent—a behavior that can cause subtle bugs, especially when handling HTTP status codes, configuration values, or data from external APIs.
In this article, we’ll explore the technical and practical differences between switch and match. We’ll examine not just the mechanics of how match works, but also the design decisions behind it and the trade-offs to consider when choosing between these constructs. Our goal is to equip you with the knowledge to make sustainable choices for your codebase.
Prerequisites
Before we dive into the match expression, you should be aware of a few prerequisites:
- PHP version: You need PHP 8.0 or later to use
match. If you’re still on PHP 7.x, this feature won’t be available. You can check your PHP version withphp -v. - Basic understanding: This article assumes you’re familiar with
switchstatements and have written PHP code before. If you’re just starting with PHP, you might want to gain some experience withswitchfirst. - Testing environment: We strongly recommend having a development environment where you can safely test code examples—whether that’s a local PHP installation, a Docker container, or an online sandbox.
Keep in mind that if you maintain a codebase with many switch statements, refactoring to match will require PHP 8.0+ throughout your entire deployment environment. This is an important consideration if you’re still supporting older PHP versions in production.
Understanding the Challenge
When we need to execute different code paths based on a single value, we’ve historically had a few options in PHP. The most straightforward is an if/elseif chain:
if ($status === 200) {
return 'OK';
} elseif ($status === 201) {
return 'Created';
} elseif ($status === 400) {
return 'Bad Request';
} // ... and so on
This works, but as the number of cases grows, it becomes unwieldy. The switch statement, introduced in PHP 4.0, offered a cleaner syntax for this pattern. For over twenty years, switch has been the standard approach for multi-way branching in PHP.
However, when we look at the PHP ecosystem today, we see that switch carries several design limitations that have bitten developers repeatedly: loose type comparison that can cause unexpected matches, mandatory break statements that are easy to forget, and no guarantee that all possible cases are handled. These aren’t theoretical concerns—they manifest as production bugs that can be subtle and hard to catch.
We should be clear that switch isn’t going away. It remains a valid part of PHP 8.0 and later. But for many scenarios where we’re mapping inputs to outputs, match provides a compelling alternative that addresses these long-standing issues.
Consider this example of a switch statement that returns the day of the week:
function getDayOfWeek(int $day): string {
$dayName = '';
switch ($day) {
case 1:
$dayName = 'Monday';
break;
case 2:
$dayName = 'Tuesday';
break;
case 3:
$dayName = 'Wednesday';
break;
case 4:
$dayName = 'Thursday';
break;
case 5:
$dayName = 'Friday';
break;
case 6:
$dayName = 'Saturday';
break;
case 7:
$dayName = 'Sunday';
break;
default:
$dayName = 'Invalid day';
break;
}
return $dayName;
}
This code works, but notice the pattern: we declare a variable $dayName upfront, then assign to it in each case. We must remember to include break statements to prevent fall-through behavior—which, if forgotten, can cause serious bugs. The verbosity adds noise without adding value.
The New Way: match Expressions
The match expression, introduced in PHP 8.0, simplifies this logic. Here’s the same function rewritten using match:
function getDayOfWeek(int $day): string {
return match ($day) {
1 => 'Monday',
2 => 'Tuesday',
3 => 'Wednesday',
4 => 'Thursday',
5 => 'Friday',
6 => 'Saturday',
7 => 'Sunday',
default => 'Invalid day',
};
}
Let’s examine what changed here. Notice that we’re returning the match expression directly—no need to declare a variable upfront. Each arm of the match consists of a condition (the value to match) followed by => and the result expression. We don’t need break statements; each arm is self-contained.
But the match expression isn’t merely about conciseness. It addresses several of the switch statement’s long-standing issues. Let’s explore those differences in detail.
Key Differences and Advantages of match
Let’s explore the four key differences that make match a valuable tool in your PHP 8.0+ toolbox. We’ve already seen the conciseness benefit—now let’s examine the deeper advantages.
Type Comparison: Strict vs. Loose
One significant difference is that match uses strict type comparison (===), while switch uses loose comparison (==). This means match will distinguish between values like the integer 1 and the string "1", whereas switch would treat them as equivalent in many cases.
Consider this practical example. Suppose you’re handling HTTP status codes from user input—data that comes from a form field and arrives as strings:
// Simulate a status code from form input (string)
$statusCode = '200';
switch ($statusCode) {
case 200:
echo 'Success';
break;
case '200':
echo 'Also success (but this branch is never reached)';
break;
case 404:
echo 'Not found';
break;
default:
echo 'Other';
}
What’s the output? You might expect “Success,” but you’d be wrong. Actually, this prints “Also success” because switch’s loose comparison treats '200' as equal to 200. However, the first case (case 200) matches, and we fall through to the second case because we forgot the break—ah, the classic switch pitfall! Let’s fix that:
$statusCode = '200';
switch ($statusCode) {
case 200:
echo 'Success'; // This actually prints
break;
case '200':
echo 'This never reaches here';
break;
// ...
}
// Output: "Success"
Now, what does match do?
$statusCode = '200';
echo match ($statusCode) {
200 => 'Success',
'200' => 'String 200',
404 => 'Not found',
default => 'Other',
};
// Output: "String 200"
Aha! The match expression correctly identifies that '200' matches the '200' string case, not the integer 200 case. This behavior is much more predictable, especially when dealing with data from external sources—user input, API responses, database values—where type ambiguity is common.
Of course, there are scenarios where you might want loose comparison. If you’re working with data that could be either strings or integers that should be treated equivalently, switch still has a place. But for most modern PHP code, where we’re increasingly conscious of type safety, match’s strict comparison prevents a whole class of subtle bugs.
Return Values: Expressions, Not Statements
The match construct is an expression, meaning it produces a value. The switch statement is a statement—it performs an action but doesn’t inherently produce a value you can assign or return directly.
What does this mean in practice? With match, you can write:
// Direct return
function getHttpMessage(int $code): string {
return match ($code) {
200 => 'OK',
201 => 'Created',
301 => 'Moved Permanently',
400 => 'Bad Request',
401 => 'Unauthorized',
403 => 'Forbidden',
404 => 'Not Found',
500 => 'Internal Server Error',
default => 'Unknown',
};
}
// Or assign directly
$message = match ($statusCode) {
200 => 'Success',
default => 'Error',
};
Compare that to the switch equivalent, where you need to:
function getHttpMessage(int $code): string {
$message = '';
switch ($code) {
case 200:
$message = 'OK';
break;
case 201:
$message = 'Created';
break;
// ... and so on for each case
default:
$message = 'Unknown';
break;
}
return $message;
}
Notice the boilerplate: we declare $message upfront, assign in each branch, then return. It works, but it’s more verbose and introduces the risk of forgetting to set $message in some branch (which would result in an undefined variable error).
Here’s a practical tip: When converting switch to match, look for places where the switch assigns to a variable that’s returned or used afterward. Those are excellent candidates for replacement.
No Fall-Through: Safety by Design
We’ve all been there: you add a new case to a switch, forget the break statement, and suddenly code from another case executes unexpectedly. This “fall-through” behavior is one of the most common sources of switch-related bugs.
// UH-OH: Forgot the break!
$day = 3;
switch ($day) {
case 1:
echo 'Monday';
case 2:
echo 'Tuesday';
case 3:
echo 'Wednesday';
case 4:
echo 'Thursday';
// ...
}
// Output: "WednesdayThursdayFridaySaturdaySunday"
// We fell through all remaining cases!
This kind of bug can be subtle—it might not show up until a rare edge case hits production.
match eliminates this entire class of errors. Each arm is self-contained; there’s no fall-through. If you want multiple conditions to map to the same action, you can use a comma-separated list:
$day = 3;
echo match ($day) {
1, 2, 3, 4, 5 => 'Weekday',
6, 7 => 'Weekend',
default => 'Invalid',
};
// Output: "Weekday"
One may wonder: Why did PHP introduce an entirely new construct rather than modifying switch to require explicit break statements? That would be a breaking change for existing code. By introducing match as a separate expression, PHP gives us a safer option for new code while maintaining backward compatibility—a pragmatic trade-off.
Unhandled Cases: Fail Fast
What happens when a switch receives a value that doesn’t match any case? It does nothing and continues execution. Sometimes that’s fine; other times, it’s a silent failure.
$status = 418; // "I'm a teapot" - an uncommon HTTP code
switch ($status) {
case 200:
echo 'OK';
break;
case 404:
echo 'Not Found';
break;
// No default case
}
// No output at all. Silent failure.
With match, if you omit a default arm and the value doesn’t match any case, PHP throws an UnhandledMatchError. This is a fail-fast behavior: you discover missing cases immediately during development or testing, rather than in production when an unexpected value appears.
$status = 418;
try {
echo match ($status) {
200 => 'OK',
404 => 'Not Found',
// No default
};
} catch (UnhandledMatchError $e) {
echo 'Unhandled: ' . $e->getMessage();
// Output: "Unhandled match value of type int: 418"
}
If you want the safety of exhaustive matching but don’t want to provide a fallback, you must list all possible cases. This forces you to think about all the values your code might encounter.
When to Use match vs. switch: A Decision Framework
The match expression offers several advantages over switch, but it also has limitations that mean switch remains useful in certain situations. Rather than thinking of this as an either/or choice, it’s more helpful to understand the characteristics of each and when one is more appropriate than the other.
Use match When:
We typically reach for match in the following scenarios:
- We need a return value directly - We’re mapping an input to an output in a single expression. For example, converting HTTP status codes to messages or translating enum values to labels.
- Type safety matters - We need strict comparison to avoid subtle bugs, especially when dealing with user input, API responses, or database values that may have ambiguous types.
- Fall-through would be dangerous - If missing
breakstatements have caused issues in our codebase (or we want to eliminate that entire class of errors),matchprovides safety by design. - We want exhaustive checking - We prefer our code to fail fast on unhandled cases during development rather than silently ignoring unexpected values in production.
- The logic is straightforward per case - Each branch consists of a single expression rather than multiple statements.
Example: HTTP status code mapping (good for match)
Here’s a concrete example where match is particularly well-suited:
function getStatusLabel(int $code): string {
return match ($code) {
200 => 'OK',
201 => 'Created',
400 => 'Bad Request',
401 => 'Unauthorized',
403 => 'Forbidden',
404 => 'Not Found',
500 => 'Internal Server Error',
default => throw new InvalidArgumentException("Unknown status: $code"),
};
}
Notice we’re using throw as the default—this creates truly exhaustive matching. We could also omit default entirely and let PHP throw UnhandledMatchError, which can be helpful during development to ensure we’ve considered all possibilities.
Use switch When:
Of course, switch still has its place. We tend to prefer it in these scenarios:
- We need multiple statements per case - Each branch performs several operations. While we could wrap multiple statements in a closure within
match, it often becomes less readable than a straightforwardswitch. - Fall-through behavior is explicitly desired - This is relatively rare, but there are cases where intentional cascading makes sense (though it’s often a code smell that suggests refactoring).
- Loose comparison is necessary - We specifically want type coercion, treating
'1'and1as equivalent. This can be appropriate when normalizing user input that might arrive in different formats. - We’re maintaining code that must support PHP versions prior to 8.0 - If our deployment environment includes PHP 7.x,
matchisn’t available. - We’re performing actions rather than returning a value - When each branch needs to execute multiple imperative statements that modify state, log, or have side effects,
switchtends to be clearer.
Example: Multi-step initialization requiring multiple statements
Consider a function that sets up different user roles:
function initializeUserContext(string $role, int $userId): void {
switch ($role) {
case 'admin':
$permissions = getAdminPermissions();
logAdminAccess($userId);
initializeAdminDashboard();
// More setup as needed...
break;
case 'moderator':
$permissions = getModeratorPermissions();
logModeratorAccess($userId);
initializeModeratorTools();
break;
case 'subscriber':
$permissions = getSubscriberPermissions();
trackSubscriberLogin($userId);
break;
default:
// Unknown role, fall back to defaults
$permissions = getDefaultPermissions();
break;
}
// Continue with setup that uses $permissions...
}
Could we rewrite this with match? We could return an array or callable and then execute it, but it becomes more convoluted. For this pattern, switch remains the clearer choice.
A Practical Migration Strategy
If we’re maintaining an existing PHP 7.x codebase and planning to upgrade to PHP 8.0+, we don’t need to convert all switch statements at once. Here’s a practical approach we can follow:
- Audit first: We can use static analysis tools like PHPStan or Psalm to identify all
switchstatements in our codebase. Some teams also use simple grep searches:grep -r "switch" app/ src/. - Prioritize: We should focus on
switchstatements that:- Return values directly (these are the easiest and lowest-risk to convert)
- Have had bugs related to fall-through or type comparison issues in the past
- Are in critical paths where safety and correctness are paramount
- Convert incrementally: We replace one at a time, testing thoroughly after each change. The good news?
matchandswitchare syntactically distinct, so we can safely convert without worrying about subtle behavioral changes—provided we understand the strict vs. loose comparison difference. It’s wise to run our test suite with both versions in a feature branch before merging. - Leave some as
switch: When the use case clearly favorsswitch(multiple statements per case, needed loose comparison), we leave it. Both constructs are valid in PHP 8.0+, and using the right tool for the job matters more than dogmatic conversion.
Of course, if we’re writing new code in PHP 8.0+, we should start with match as our default for value mapping. We can reach for switch only when we have a specific reason that aligns with the scenarios we discussed.
Edge Cases: What match Can’t Do (Yet)
It’s worth mentioning what match doesn’t support, so you don’t try to force it where it doesn’t fit:
- Multiple statements per arm: Each arm must be a single expression. If you need multiple statements, either use
switchor wrap the logic in a closure or function call. - Conditional matching beyond equality:
matchonly supports strict equality (===). If you need range checks, pattern matching, or other conditions, you’ll still needswitchwith complex case statements or a series ofif/elseif. - Variable case values: Case values must be constant expressions (literals, constants, enum cases). You can’t evaluate functions or expressions at runtime for case labels.
- Falling through: There’s deliberately no way to enable fall-through in
match. If you need that behavior, useswitch.
Common Pitfalls and Gotchas
As you begin using match in your codebase, watch out for these common pitfalls:
UnhandledMatchError in Production
If you forget a default arm or omit some possible values, match will throw UnhandledMatchError. This is great for catching bugs early, but it can cause production outages if you’re not prepared.
Problem: You convert a switch to match and don’t account for all possible values. The code worked fine before because switch silently did nothing for unhandled values. Now it throws an exception.
Solution: Either provide a default arm or ensure you’ve explicitly listed all possible values. When you first convert, consider wrapping in try-catch temporarily until you’re confident all cases are covered:
try {
$result = match ($value) {
'pending' => handlePending(),
'active' => handleActive(),
'inactive' => handleInactive(),
// Did we forget anything?
};
} catch (UnhandledMatchError $e) {
// Log and handle gracefully during transition
error_log('Unhandled value: ' . $value);
$result = handleDefault();
}
Strict Comparison Surprises
You might expect match to behave like switch and be surprised when it doesn’t.
Problem: Converting a switch that relies on type coercion:
// Original switch (loose comparison)
switch ($input) {
case 'yes':
return true;
case 'no':
return false;
case 1:
return true; // also matches 'yes' loosely
case 0:
return false; // also matches 'no' loosely
}
After converting to match, suddenly some inputs that worked before now hit default or behave differently.
Solution: Review your switch statements before converting. If they rely on loose comparison, either:
- Keep them as
switch - Add explicit cases for both types
Match Returns Values, Not Execution Context
Remember: match is an expression that returns a value. You cannot use it like a switch statement to execute arbitrary code without capturing the result.
Problem: Writing match without assigning or returning its value:
// This does nothing!
match ($status) {
200 => logSuccess(),
404 => logNotFound(),
default => logError(),
};
// The return value is discarded
Solution: Either assign the result, return it, or use it in some way:
$logMessage = match ($status) {
200 => 'Success',
404 => 'Not found',
default => 'Error',
};
error_log($logMessage);
Single Expression Per Arm
If you find yourself trying to put multiple statements in a match arm, you’ve chosen the wrong tool.
Problem:
// Won't work - multiple statements
match ($role) {
'admin' => {
$permissions = getAdminPermissions();
logAccess($userId);
$permissions;
},
// ...
}
Solution: Use switch, or restructure to return a value from a closure:
$permissions = match ($role) {
'admin' => (function() use ($userId) {
logAccess($userId);
return getAdminPermissions();
})(),
'moderator' => getModeratorPermissions(),
default => getDefaultPermissions(),
};
Though honestly, the switch statement is clearer here.
Missing break is No Longer a Concern (That’s Good!)
You might look at old switch code and worry about missing break statements. With match, this is impossible—there’s no fall-through. This is one of the main benefits, so enjoy it!
PHP Version Compatibility
Problem: You deploy to servers running different PHP versions. Your local dev environment has PHP 8.0+, but production still runs PHP 7.4. You use match and everything breaks in production.
Solution: Check your PHP version requirements in composer.json:
{
"require": {
"php": "^8.0"
}
}
Ensure all your deployment targets meet the minimum version before using match. If you need to maintain backward compatibility, keep those switch statements until you can upgrade everywhere.
Troubleshooting
Here are solutions to common issues you might encounter when working with match.
”UnhandledMatchError”Exception
Symptom: Your code throws UnhandledMatchError at runtime.
Cause: The value passed to match doesn’t match any arm, and you didn’t provide a default arm.
Resolution: Add a default arm to handle unexpected values:
$result = match ($value) {
'expected1' => handle1(),
'expected2' => handle2(),
default => handleUnexpected($value), // or throw a custom exception
};
Alternatively, if you want to be exhaustive without a default, ensure you’ve enumerated all possible values (including enum cases, null, etc.).
“match” Does Not Recognize My Case
Symptom: You expect a value to match a case, but it hits default instead.
Cause: Most likely a type mismatch. match uses strict comparison (===). The value’s type doesn’t match the case’s type.
Resolution: Check the type of your input:
var_dump($value); // What type is it? string? int? null?
Adjust your case values to match the type exactly:
// If $value is string '200', you need:
$result = match ($value) {
'200' => 'String 200', // string case
default => 'Other',
};
// Not:
// 200 => 'String 200', // This is integer 200, won't match string '200'
“Cannot use object of type X as array” or Type Errors
Symptom: You get type errors after converting switch to match.
Cause: With match, you often return values directly. If your switch had implicit fall-through or assigned to different variables, the match conversion may have changed what gets returned.
Resolution: Carefully map each switch case to its intended return value. Make sure all arms return the same type (or compatible types):
// Bad: inconsistent return types
$result = match ($code) {
200 => ['status' => 'ok'], // array
404 => 'Not found', // string
default => null, // null
};
// Better: ensure consistent types or use match with union return type
$result = match ($code) {
200, 404 => ['status' => $code === 200 ? 'ok' : 'not found'],
default => throw new InvalidArgumentException(),
};
Performance Concerns
Question: Is match slower than switch?
Answer: The performance difference is negligible for most applications. match is implemented as an optimized hash table lookup in PHP 8.0+, similar to switch with integer or string keys. Don’t optimize prematurely—choose based on correctness and readability.
”match” with Null Values
Symptom: match doesn’t handle null values as expected.
Cause: match uses strict comparison, so null only matches null, not an empty string or zero.
Resolution: Explicitly handle null if it’s a possibility:
$result = match ($value) {
null => 'No value provided',
'' => 'Empty string',
0 => 'Zero',
default => 'Something else',
};
Using match with Objects or Arrays
Question: Can I match on objects or arrays?
Answer: Yes, but be cautious. match uses === comparison. For objects, this means identity (same instance), not equality of properties. For arrays, it’s deep equality but order matters.
// Object comparison by identity - rarely useful
$obj1 = new stdClass();
$obj2 = $obj1;
$obj3 = new stdClass();
match ($obj1) {
$obj1 => 'Same instance', // matches
$obj2 => 'Same instance', // also matches (same instance)
$obj3 => 'Different instance', // won't happen
};
Usually, you’ll match on scalar values (strings, ints, bools) or enum cases.
Backporting match to PHP 7.x
Question: Can I use match in PHP 7.x?
Answer: No, match is a PHP 8.0+ feature. If you need similar behavior in older versions, you can:
- Use
switchwith careful discipline - Use a polyfill library (search Packagist for “match polyfill”)
- Upgrade to PHP 8.0+ (recommended)
Testing Your Refactoring
After converting switch to match, be sure to test:
- Unit tests: If you have existing tests for the
switchlogic, they should still pass—unless the type behavior difference surfaces a bug that was hidden before. That’s a good thing! - Integration tests: Ensure the refactored code works correctly in context.
- Type coverage: Use tools like PHPStan or Psalm to verify type safety. The strict typing of
matchoften improves type analysis. - Edge cases: Test with boundary values, nulls, unexpected types.
A practical tip: Run your test suite with both the old switch code and the new match code side by side in a feature branch. If all tests pass and PHPStan/Psalm show no errors, you’re likely in good shape.
Conclusion
The match expression introduced in PHP 8.0 provides developers with an additional tool for handling conditional logic. While it offers benefits like strict comparison and return values in certain situations, it also has limitations that mean switch remains valuable for other use cases.
Understanding the strengths and weaknesses of both approaches allows developers to choose the most appropriate tool for each specific situation in their PHP applications.
Sponsored by Durable Programming
Need help with your PHP application? Durable Programming specializes in maintaining, upgrading, and securing PHP applications.
Hire Durable Programming