Leveraging PHP 8's Match Expression for Cleaner Control Flow
Why Match Is Becoming My Go‑To Control Flow Statement
Over the years I’ve used switch in nearly every project, but PHP 8 introduced match as a more expressive alternative. At first I treated it as a novelty, yet after a few real‑world uses I realized it can reduce bugs, improve readability, and even help static analyzers catch missing cases. In this article I’ll share why I now reach for match and how you can apply it in production code.
A Quick Comparison: Switch vs. Match
The classic switch statement works well for equality checks, but it has several quirks. It falls through unless you explicitly add break, it cannot return a value directly (you must use return or echo), and it lacks built‑in support for strict typing. match eliminates those issues by design:
- It always returns a value (or
null if none matches). - No fall‑through; each case is isolated.
- Supports types (including unions) and can be used with enums.
- Static analysis tools can warn you about non‑exhaustive patterns.
Because of these properties, match feels more like a functional if/elseif chain while retaining the clarity of a switch.
Real‑World Scenario: Routing HTTP Methods to Handlers
In a typical REST API you map an HTTP verb to a controller action. Previously I wrote something like:
$method = $_SERVER['REQUEST_METHOD'];
$handler = null;
switch ($method) {
case 'GET':
$handler = new GetHandler();
break;
case 'POST':
$handler = new PostHandler();
break;
case 'PUT':
$handler = new PutHandler();
break;
case 'DELETE':
$handler = new DeleteHandler();
break;
default:
http_response_code(405);
exit('Method Not Allowed');
}
While functional, the code is verbose and relies on the developer to remember break. With PHP 8 I can rewrite it using match:
$method = $_SERVER['REQUEST_METHOD'];
$handler = match ($method) {
'GET' => new GetHandler(),
'POST' => new PostHandler(),
'PUT' => new PutHandler(),
'DELETE' => new DeleteHandler(),
default => throw new InvalidArgumentException('Method Not Allowed'),
};
$handler->process();
Notice the simplicity: each case returns a value, there is no risk of fall‑through, and the default case throws an exception rather than silently exiting. The intent is clearer because the assignment operator directly ties the result to $handler.
Exhaustive Pattern Matching
One of my favorite features is that match can be made exhaustive. If you list all possible values, tools like Psalm or PHPStan will flag a missing case. For example, when handling API status codes I might write:
$status = $response['status'];
$message = match ($status) {
'success' => 'Operation completed',
'error' => 'Something went wrong',
'pending' => 'Still processing',
// No default – we want to know if we missed a case
};
// If $status is not one of the three above, static analysis warns us.
If a new status like 'canceled' is added later, the analyzer will surface the oversight. This safety net is a big win for long‑lived codebases where statuses evolve.
Using Match with Complex Types
Unlike switch, match supports type patterns. You can match against objects, arrays, or even unions. Suppose you have a generic logger that accepts either a string or an array of context:
$logEntry = match (true) {
is_string($payload) => ['message' => $payload, 'level' => 'info'],
is_array($payload) => array_merge(['level' => 'debug'], $payload),
default => throw new InvalidPayloadException(),
};
logger($logEntry);
Here we used match (true) to evaluate a condition, but the structure remains readable and each branch can return a distinct shape. This pattern is handy when you need to normalize input before processing.
Tip: When you need to map a single expression to multiple outcomes, prefer
match. It enforces a clear one‑to‑one mapping and eliminates the accidental fall‑through that can hide bugs inswitch statements.
When to Stick with Switch
Match isn’t a silver bullet. If you need to perform side effects without returning a value, or you want to use case labels that are not simple equality checks (e.g., ranges), switch is still appropriate. For example, PHP’s date functions often use switch (date('w'))` to map weekday numbers to abbreviations. In such cases the logic is more procedural than functional, and switch remains idiomatic.
Putting It All Together: A Production‑Ready Helper
To demonstrate a real‑world helper, I built a simple service that normalizes HTTP status codes to human‑readable messages, falling back to a default if the code is unknown. The function uses match for clarity and includes proper type hints.
'Continue',
200 => 'OK',
201 => 'Created',
204 => 'No Content',
400 => 'Bad Request',
401 => 'Unauthorized',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
500 => 'Internal Server Error',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
default => 'Unknown Status',
};
}
// Example usage:
echo httpStatusDescription(404); // Not Found
?>
The function is self‑documenting; each case is a literal mapping. Adding a new status is as simple as inserting a new line, and the IDE will highlight any gaps if you enable exhaustive checks.
Final Thoughts
I’ve found that match shines when you have a single expression that needs to be compared against a fixed set of values and you want a result back. It reduces boilerplate, eliminates common mistakes like missing break, and works nicely with modern tooling. While switch still has its place for procedural branching, reaching for match first often leads to cleaner, more maintainable code.
Adopting match is a small upgrade that pays dividends in readability and safety. Give it a try on your next mapping problem—you’ll likely wonder how you lived without it.