DEV Community

Cover image for The `match` Expression in PHP
Ash Allen
Ash Allen

Posted on • Originally published at ashallendesign.co.uk

The `match` Expression in PHP

Introduction

The match expression is a PHP feature that I love using. It was introduced in PHP 8.0 (released in November 2020), so it's been around for a while now. But I thought I'd put together a Quickfire article about it to help spread the word, for anyone (such as those new to PHP) who may not be aware of it.

The match Expression in PHP

The match expression allows you to compare a value against multiple conditions and return a given result (although a result doesn't necessarily have to be returned). It's similar to a switch statement in the sense that you can define "branches" based on different conditions.

However, a key difference is that the match expression uses strict comparison (===), whereas a switch statement uses loose comparison (==). This means that with match, the types of the values being compared must be the same for a match to occur.

I think the best way to understand what a match expression is is to see it in action. So let's take a look at an example of how we might use it in a real-world scenario.

We'll imagine we have an App\Services\Git\RepositoryManager class that allows us to interact with different Git repository hosting services, such as GitHub, GitLab, and Bitbucket. Each service has its own driver class that implements a common interface called App\Interfaces\Git\Drivers\RepositoryDriver. Using if/elseif/else, we might write something like this:

namespace App\Services\Git;

use App\Interfaces\Git\Drivers\RepositoryDriver;
use App\Services\Git\Drivers\BitbucketDriver;
use App\Services\Git\Drivers\GitHubDriver;
use App\Services\Git\Drivers\GitLabDriver;

final readonly class RepositoryManager
{

    // ...

    public function driver(string $driver): RepositoryDriver
    {
        if ($driver === 'github') {
            return new GitHubDriver();
        } elseif ($driver === 'gitlab') {
            return new GitLabDriver();
        } elseif ($driver === 'bitbucket') {
            return new BitbucketDriver();
        } else {
            throw new \InvalidArgumentException("Unsupported driver: $driver");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, with the above code example, we can do something like this to get a driver instance for interacting with GitHub:

$githubDriver = new RepositoryManager()->driver('github');
Enter fullscreen mode Exit fullscreen mode

However, we can simplify our driver method using a match expression, like so:

namespace App\Services\Git;

use App\Interfaces\Git\Drivers\RepositoryDriver;
use App\Services\Git\Drivers\BitbucketDriver;
use App\Services\Git\Drivers\GitHubDriver;
use App\Services\Git\Drivers\GitLabDriver;

final readonly class RepositoryManager
{

    // ...

    public function driver(string $driver): RepositoryDriver
    {
        return match ($driver) {
            'github' => new GitHubDriver(),
            'gitlab' => new GitLabDriver(),
            'bitbucket' => new BitbucketDriver(),
            default => throw new \InvalidArgumentException("Unsupported driver: $driver"),
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

In the code example above, we have used the match expression to define 4 separate branches. The first 3 branches check if the $driver variable matches one of the supported drivers, and if so, it returns a new instance of the corresponding driver class. The default branch is used to handle any unsupported driver values by throwing an \InvalidArgumentException.

In my personal opinion, I now find this function much easier to read and understand at a glance. I also like that it's reduced the number of return statements in the method. Although there's nothing wrong with having multiple return statements, I find it easier to understand the flow of the method when there's a single return point.

Multiple Conditions per Branch

match expressions can also have multiple conditions for a single branch.

For example, imagine we have our own self-hosted Git service that uses the same API as GitHub. As a result, we can use the App\Services\Git\Drivers\GitHubDriver for both GitHub and our custom Git service. We'll assume that we are referring to this driver name as 'self-hosted'. Let's update our driver method to account for this:

use App\Interfaces\Git\Drivers\RepositoryDriver;
use App\Services\Git\Drivers\BitbucketDriver;
use App\Services\Git\Drivers\GitHubDriver;
use App\Services\Git\Drivers\GitLabDriver;

namespace App\Services\Git;

final readonly class RepositoryManager {

    // ...

    public function driver(string $driver): RepositoryDriver
    {
        return match ($driver) {
            'github', 'self-hosted' => new GitHubDriver(),
            'gitlab' => new GitLabDriver(),
            'bitbucket' => new BitbucketDriver(),
            default => throw new \InvalidArgumentException("Unsupported driver: $driver"),
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

We can see in the code example above that we have added 'self-hosted' as an additional condition for the first branch. This means that if the $driver variable is either 'github' or 'self-hosted', it will return a new instance of the App\Services\Git\Drivers\GitHubDriver.

Passing true to the match Expression

A cool use case for the match expression is to pass true to it. This allows you to then evaluate conditions in the arms of the match statement.

As a basic example, let's say we want to set a message based on a user's role:

// Assume we have retrieved a user from the database
// and that the user is an admin.
$user = \App\Models\User::first();

$message = match (true) {
    $user->isAdmin() => 'User is an admin',
    $user->isEditor() => 'User is an editor',
    $user->isSubscriber() => 'User is a subscriber',
    default => 'User role is unknown',
};

// $message will be 'User is an admin'
Enter fullscreen mode Exit fullscreen mode

Pairing Match with Enums

One of the places that I've found match expressions pair really nicely is with PHP enums. For example, let's say we have an enum that defines different job types:

enum JobType: string
{
    case WebDeveloper = 'web_developer';

    case Designer = 'designer';

    case ProjectManager = 'project_manager';

    case SalesManager = 'sales_manager';
}
Enter fullscreen mode Exit fullscreen mode

Although we might want to use the raw enum values in some places (such as the database and application code), we wouldn't want to display these to the user. For instance, if the user can select one of the job type options in a form, we'd much rather show them a more user-friendly label, such as "Web Developer" instead of "web_developer".

So let's add a toFriendly method to our JobType enum that uses a match expression to return a user-friendly label for each job type:

enum JobType: string
{
    case WebDeveloper = 'web_developer';

    case Designer = 'designer';

    case ProjectManager = 'project_manager';

    case SalesManager = 'sales_manager';

    public function toFriendly(): string
    {
        return match ($this) {
            self::WebDeveloper => 'Web Developer',
            self::Designer => 'Designer',
            self::ProjectManager => 'Project Manager',
            self::SalesManager => 'Sales Manager',
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

You may have noticed that we've not defined a default branch in our match expression. This is because our expression is exhaustive, meaning that all the possible cases have been covered, so the match expression can't evaluate to anything other than one of the defined cases.

Now we can use the toFriendly method to get a user-friendly label for any JobType enum value:

$jobType = JobType::WebDeveloper;

echo $jobType->toFriendly(); // Outputs: Web Developer
Enter fullscreen mode Exit fullscreen mode

In my opinion, pairing the match expression with enums like this is a great way to keep your code clean and maintainable. I feel like they just work really well together.

An alternative approach you could take to adding helper methods, such as toFriendly, to your enums could be to use PHP attributes. I have an article which covers how to do this, if you're interested: A Guide to PHP Attributes.

Conclusion

Hopefully, this article has given you a quick overview of the match expression in PHP, along with some practical examples of how you can use it in your own code.

If you enjoyed reading this post, you might be interested in checking out my 220+ page ebook "Battle Ready Laravel" which covers similar topics in more depth.

Or, you might want to check out my other 440+ page ebook "Consuming APIs in Laravel" which teaches you how to use Laravel to consume APIs from other services.

If you're interested in getting updated each time I publish a new post, feel free to sign up for my newsletter.

Keep on building awesome stuff! 🚀

Top comments (1)

Collapse
 
xwero profile image
david duymelinck

I understand the examples are there to showcase the match method.
And I agree that match is an excellent standard library function.

For the function driver(string $driver) example without the multiple matching options, i think it is better to pass an enum.

enum RepositoryDriver : string
{
    case GitHub = 'App\Services\Git\Drivers\GitHubDriver';
  // other drivers
}

function driver(RepositoryDriverEnum $driver) : RepositoryDriver
{
   return new {$driver->value}();
}
Enter fullscreen mode Exit fullscreen mode

This allows you to keep the driver options centralized, and you don't need default logic.

For the match(true) example, I would add an getRole or getHighestRole method to the model.
The idea behind that is again skipping the default logic and providing more context.

My comment comes down to understand all the options of match and use them wisely.