Introduction
The match expression, introduced in PHP 8.0, is a more powerful and safer alternative to switch. It uses strict comparison, returns a value, has no fall-through, and throws an error when no arm matches. This lesson covers both basic and advanced usage patterns.
Key Concepts
- Match Expression: A compact alternative to
switchthat uses strict (===) comparison and returns a value. - No Fall-Through: Each arm is independent — no
breakstatements needed. match(true)Pattern: Usingtrueas the subject to evaluate boolean expressions in each arm.
Real World Context
The switch statement has been a source of bugs for decades: loose comparison (==), accidental fall-through, and no return value. The match expression eliminates all three problems. It is now the recommended way to branch on values in modern PHP.
Deep Dive
Match Returns Values
Unlike switch, match is an expression that returns a value:
php<?php $status = 'active'; $message = match($status) { 'active' => 'User is currently active', 'pending' => 'Awaiting activation', 'banned' => 'User has been banned', default => 'Unknown status', }; echo $message;
The result is assigned directly to a variable — no temporary variable or break needed.
Multiple Values Per Arm
Group multiple values with commas:
php<?php $day = 'Saturday'; $type = match($day) { 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday' => 'Weekday', 'Saturday', 'Sunday' => 'Weekend', };
This is cleaner than switch with fall-through cases.
Matching with Expressions: match(true)
Use match(true) to evaluate conditions:
php<?php $age = 25; $category = match(true) { $age < 13 => 'child', $age < 20 => 'teenager', $age < 30 => 'young adult', $age < 60 => 'adult', default => 'senior', };
Each arm is a boolean expression. The first one that evaluates to true wins.
Throwing in Match Arms
Throw exceptions directly:
php<?php function getStatusCode(string $status): int { return match($status) { 'ok' => 200, 'created' => 201, 'not_found' => 404, 'error' => 500, default => throw new ValueError("Invalid status: $status"), }; }
The throw expression (PHP 8.0) works seamlessly in match arms.
Match vs Switch: Strict Comparison
This is the most important behavioral difference:
php<?php // Switch uses loose comparison (==) switch ($value) { case 0: case '0': // Both match 0 due to loose comparison! $result = 'zero'; break; } // Match uses strict comparison (===) $result = match($value) { 0 => 'integer zero', '0' => 'string zero', // Different from integer 0 default => 'other', };
With match, 0 and '0' are different values. This prevents the most common class of switch bugs.
Match with Enums
Enums and match are a natural pairing:
php<?php enum Status { case Active; case Inactive; case Pending; } $color = match($status) { Status::Active => 'green', Status::Inactive => 'gray', Status::Pending => 'yellow', };
If you omit a case and there is no default, PHP throws an UnhandledMatchError, ensuring exhaustive handling.
Common Pitfalls
- Missing
defaultwithout exhaustive arms — If no arm matches and there is nodefault, PHP throws anUnhandledMatchError. This is actually a feature for enums (ensures exhaustiveness), but can be surprising for string inputs. - Expecting fall-through —
matchhas no fall-through. Use comma-separated values to group cases, not sequential arms.
Best Practices
- Use
matchinstead ofswitchby default — Strict comparison and no fall-through makematchsafer. Only useswitchif you need fall-through or statement bodies. - Omit
defaultfor enums — Without adefault, adding a new enum case forces you to handle it everywherematchis used, preventing missed cases.
Summary
matchis an expression: it returns a value and uses strict comparison.- No fall-through — each arm is independent.
- Use
match(true)for condition-based branching. - Throw expressions work inside match arms.
- Omit
defaultfor enums to get compile-time exhaustiveness checking.
Code Examples
<?php
declare(strict_types=1);
class ResponseBuilder {
public static function fromStatusCode(int $code): array {
return match(true) {
$code >= 200 && $code < 300 => [
'type' => 'success',
'message' => match($code) {
200 => 'OK',
201 => 'Created',
204 => 'No Content',
default => 'Success',
},
],
$code >= 400 && $code < 500 => [
'type' => 'client_error',
'message' => match($code) {
400 => 'Bad Request',
401 => 'Unauthorized',
403 => 'Forbidden',
404 => 'Not Found',
default => 'Client Error',
},
],
$code >= 500 => [
'type' => 'server_error',
'message' => match($code) {
500 => 'Internal Server Error',
503 => 'Service Unavailable',
default => 'Server Error',
},
],
default => ['type' => 'unknown', 'message' => 'Unknown Status'],
};
}
}
print_r(ResponseBuilder::fromStatusCode(404));
// ['type' => 'client_error', 'message' => 'Not Found']
?>