PHP8.0 中的 match 表达式

引言

match 表达式是我非常喜欢使用的 PHP 功能。它是在 PHP 8.0(2020 年 11 月发布)中引入的,所以已经存在一段时间了。但我想写一篇关于它的快速文章来帮助宣传,让可能还不了解它的人(比如 PHP 新手)知道这个功能。

PHP 中的 match 表达式

match 表达式允许你将一个值与多个条件进行比较并返回给定的结果(尽管不一定非要返回结果)。在定义基于不同条件的"分支"这一点上,它类似于 switch 语句。

然而,一个关键区别是 match 表达式使用严格比较(===),而 switch 语句使用松散比较(==)。这意味着使用 match 时,被比较值的类型必须相同才能匹配。

我认为理解 match 表达式的最佳方式是看它在实际场景中的应用。让我们来看一个在真实场景中如何使用它的例子。

假设我们有一个 App\Services\Git\RepositoryManager 类,允许我们与不同的 Git 代码仓库托管服务(如 GitHub、GitLab 和 Bitbucket)进行交互。每个服务都有其自己的驱动类,这些类实现了一个名为 App\Interfaces\Git\Drivers\RepositoryDriver 的通用接口。使用 if/elseif/else,我们可能会这样写:

php
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("不支持的驱动: $driver");
        }
    }
}

使用上面的代码示例,我们可以这样做来获取一个用于与 GitHub 交互的驱动实例:

php
$githubDriver = new RepositoryManager()->driver('github');

但是,我们可以使用 match 表达式来简化我们的 driver 方法,如下所示:

php
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("不支持的驱动: $driver"),
        };
    }
}

在上面的代码示例中,我们使用 match 表达式定义了 4 个独立的分支。前 3 个分支检查 $driver 变量是否匹配支持的驱动之一,如果匹配,则返回相应驱动类的新实例。default 分支用于处理任何不受支持的驱动值,抛出一个 \InvalidArgumentException

就我个人而言,我现在发现这个函数更容易一目了然地阅读和理解。我也喜欢它减少了方法中的 return 语句数量。虽然拥有多个 return 语句并没有错,但我发现当只有一个返回点时,更容易理解方法的流程。

每个分支的多个条件

match 表达式也可以让单个分支拥有多个条件。

例如,假设我们有自己的自托管 Git 服务,它使用与 GitHub 相同的 API。因此,我们可以将 App\Services\Git\Drivers\GitHubDriver 用于 GitHub 和我们的自定义 Git 服务。我们假设将此驱动名称称为 'self-hosted'。让我们更新我们的 driver 方法来考虑这一点:

php
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("不支持的驱动: $driver"),
        };
    }
}

我们可以在上面的代码示例中看到,我们为第一个分支添加了 'self-hosted' 作为附加条件。这意味着如果 $driver 变量是 'github''self-hosted',它将返回一个 App\Services\Git\Drivers\GitHubDriver 的新实例。

match 语句传递 true

match 表达式的一个很酷的用例是向它传递 true。这允许你在 match 语句的分支中评估条件。

作为一个基本示例,假设我们想根据用户的角色设置一条消息:

php
// 假设我们从数据库中检索到了一个用户
// 并且该用户是管理员。
$user = \App\Models\User::first();

$message = match (true) {
    $user->isAdmin() => '用户是管理员',
    $user->isEditor() => '用户是编辑',
    $user->isSubscriber() => '用户是订阅者',
    default => '用户角色未知',
};

// $message 将是 '用户是管理员'

match 与枚举配对

我发现 match 表达式与 PHP 枚举配合得非常好的一个地方是使用枚举。例如,假设我们有一个定义了不同工作类型的枚举:

php
enum JobType: string
{
    case WebDeveloper = 'web_developer';
    case Designer = 'designer';
    case ProjectManager = 'project_manager';
    case SalesManager = 'sales_manager';
}

虽然我们可能在某些地方(例如数据库和应用程序代码中)使用原始的枚举值,但我们不会希望将这些值显示给用户。例如,如果用户可以在表单中选择一个工作类型选项,我们更愿意向他们显示更用户友好的标签,比如 "Web Developer" 而不是 "web_developer"。

所以让我们向我们的 JobType 枚举添加一个 toFriendly 方法,该方法使用 match 表达式为每个工作类型返回一个用户友好的标签:

php
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',
        };
    }
}

你可能已经注意到,我们在 match 表达式中没有定义 default 分支。这是因为我们的表达式是详尽的,意味着所有可能的情况都已覆盖,所以 match 表达式只能计算为已定义的情况之一。

现在我们可以使用 toFriendly 方法为任何 JobType 枚举值获取用户友好的标签:

php
$jobType = JobType::WebDeveloper;

echo $jobType->toFriendly(); // 输出: Web Developer

在我看来,像这样将 match 表达式与枚举配对是保持代码清洁和可维护的好方法。我觉得它们配合得非常好。

向枚举添加辅助方法(如 toFriendly)的另一种替代方法是使用 PHP 属性。如果你感兴趣,我之前有一篇文章 PHP 属性指南 介绍了如何做到这一点

CatchAdmin
后端开发工程师,前端入门选手,略知相关服务器知识,偏爱❤️ Laravel & Vue
本作品采用《CC 协议》,转载必须注明作者和本文链接