Ryan Chandler

Published in PHP

All About Match Expressions

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 it's 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:

  1. match uses strict comparisons. This means that an int subject can't match a string condition.
  2. match only evaluates one condition at a time, whereas switch evaluates all case conditions before calculating the result.
  3. match only allows single-line expressions to be used in the place of [result], where as switch allows blocks of code to be executed for each case.

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 cases 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!