All About Match Expressions
This post was published 3 years ago. Some of the information might be outdated!
If you've not been keeping up-to-date with the latest PHP releases, you might have missed that PHP 8.0 introduced a new match
expression.
PHP isn't the first language to introduce the concept of a match
expression. It has existed in programming languages for quite a while, for example Rust, Scala and more recently Python.
Here's what a match
expression looks like:
match ($subject) {
[condition] => [result]
}
In its current state match
is a hyper-optimised switch
statement.
You can use it to conditionally evaluate an expression based on a [condition]
.
There are a few key differences between match
and switch
though:
-
match
uses strict comparisons. This means that anint
subject can't match astring
condition. -
match
only evaluates one condition at a time, whereasswitch
evaluates allcase
conditions before calculating the result. -
match
only allows single-line expressions to be used in the place of[result]
, where asswitch
allows blocks of code to be executed for eachcase
.
Let's take a switch
statement and convert it to a match
expression.
switch ($name) {
case 'Ryan':
echo 'Hello, Ryan!';
break;
case 'John':
echo 'Hello, John! How are you?';
break;
default:
echo 'Hello!';
}
Here's the equivalent code written in the form a match
expression:
echo match ($name) {
'Ryan' => 'Hello, Ryan!',
'John' => 'Hello, John! How are you?',
default => 'Hello!'
};
The match
expression is definitely more concise.
It's also an expression, meaning it returns a value. This is great as we can use it next to the echo
statement directly. We don't need to write an echo
for each case
like we do for a switch
statement.
Multiple conditions, one result
If we had a switch
statement that had multiple case
s executing the same block of code, we'd do something like this:
switch ($name) {
case 'Ryan':
case 'John':
echo 'Hello, Ryan or John!';
break;
default:
echo 'Hello!';
}
To achieve the same effect in a match
expression, we can separate the [condition]
expressions with commas:
echo match ($name) {
'Ryan', 'John' => 'Hello, Ryan or John!',
default => 'Hello!'
};
Generalised match statements
Imagine you have the following code:
$age = (int) $request->input('age');
if ($age < 25) {
return $this->lessThanTwentyFive();
} elseif ($age < 50) {
return $this->lessThanFifty();
} elseif ($age < 75) {
return $this->lessThanSeventyFive();
} else {
return $this->olderThanSeventyFive();
}
Personally I find elseif
statements hard to read. They make the code dense and hard to understand at a glance.
Since we're using boolean values inside of the expression, we can actually convert this into a match
expression:
$age = (int) $request->input('age');
return match (true) {
$age < 25 => $this->lessThanTwentyFive(),
$age < 50 => $this->lessThanFifty(),
$age < 75 => $this->lessThanSeventyFive(),
default => $this->oldThanSeventyFive(),
}
Method and functions calls are expressions, so we can use those in the [result]
part of the match
expression.
match
also returns a value so we can use it as the expression in a return
statement.
In the future, we could see the
(true)
part disappear with the "short match" RFC.
Array pattern matching
Although PHP doesn't support complex pattern matching (RFC), you can actually do some light pattern matching when using a match
expression with an array
.
Here's an example of how you might do something based on the values of an array:
// Pseudo-data that you might receive from the front-end.
$options = [
'monthly',
'credit-card',
];
if ($options[0] === 'monthly' && $options[1] === 'direct-debit') {
return $this->setupDirectDebit();
} else if ($options[0] === 'yearly' && $options[1] === 'credit-card') {
return $this->takeYearlyCardPayment();
} else if ($options[0] === 'monthly' && $options[1] === 'credit-card' {
return $this->setupCardSubscription();
}
Again, else if
statements can be confusing. Maybe there's a better way to write this code using match
expressions.
// Pseudo-data that you might receive from the front-end.
$options = [
'monthly',
'credit-card',
];
return match ($options) {
['monthly', 'direct-debit'] => $this->setupDirectDebit(),
['yearly', 'credit-card'] => $this->takeYearlyCardPayment(),
['monthly', 'credit-card'] => $this->setupCardSubscription(),
};
We can actually use the match
expression to match an exact array pattern. This is super handy and something that is quite common in other languages like Rust where you would match some sort of tuple pattern.
PHP doesn't have tuples, but arrays are close enough.
Beyond PHP 8.0
The match
expression in its current state is good enough to replace simple switch
statements, lookup tables and even some if..else
statements.
There are still some things that can be added to the match
expression to make it even cooler though.
Code blocks
match
only support single-line expressions at the moment. You can't have multiple lines of code as the result of a match
arm.
This was part of the original RFC, but the syntax was hard to get right so it was removed.
I'd love to see this added, perhaps with this syntax:
match ($subject) {
[condition] => {
// Write multiple lines of code here.
// Semi-colon is omitted from the last expression and is used as the return value of the block.
"Hello, world!"
}
}
It's understandable why this syntax was difficult to get right. Using return
to return a value from the block is a bit odd as that would typically return a value from the function.
Omitting the semi-colon on the last line, creating an implicit return, is also strange as that behaviour doesn't exist anywhere else in the language.
Introducing a new keyword could work, but it would be yet another keyword added to the language. PHP already has a tonne of reserved keywords.
Complex pattern matching
Pattern matching has already been discussed in this draft RFC.
It's a fairly advanced feature and has lots of use-cases which makes getting the RFC right very difficult.
Here's a simple example where we can check if an object matches a pattern based on the property values:
class Vector
{
public function __construct(
public int|float $x,
public int|float $y
) {}
}
$vector = new Vector(0, 1);
$vector is Vector { x: 0, y: 1 }; // `true`
The plan in the RFC is to introduce a new is
keyword which would act as an "infix operator" (something that goes between 2 expressions).
The part on the left would be the subject and the right would be the pattern.
In this case, the pattern is checking if $vector
is of the type Vector
and if the property $x === 0
and $y === 1
.
If we were to do this without the pattern matching, it'd look like this:
$vector instanceof Vector && $vector->x === 0 && $vector->y === 1;
It's very word-y. It's very verbose. Pattern matching is good.
Perhaps we only want to check if the $vector
object has an $x
value of 1
. We can ignore the rest of the fields in the object using the ..
token.
$vector is Vector { x: 1, .. };
Although this isn't part of the RFC, it's something that I absolutely love in Rust.
If we took this pattern matching and paired it with a match
expression, we'd end up with something like:
$vector = new Vector(0, 1);
match ($vector) {
Vector { x: 0, y: 1 } => $vector->moveTo(0, 2),
};
The pattern moves into the [condition]
spot and works exactly the same.
Some languages, like Rust, take this a step further an allow you to "bind" particular parts of a pattern to a variable in the match
's scope.
Here's what that might look like:
echo 'Y co-ordinate: ' . match ($this->getVector()) {
Vector { x: 0, $y @ y: 1 } => $y,
};
Bit of a silly example but it shows the concept. We can bind a specific part of a pattern to a variable using an @
token. We can then use the variable $y
in the expression.
Sign off
There is definitely lots of room for new features when it comes to match
expressions. Despite that, I still love them.
I've been replacing switch
statements, if..else
statements and boring array-powered lookup tables with them since upgrading to PHP 8.0.
If you haven't upgraded to PHP 8.0 yet, just do it. You're missing out on a lot of great stuff and PHP 8.1 is around the corner. Minimise your upgrade path!