Match 表达式如何结合枚举使用?

解读

这是国内 PHP 8.1+ 岗位面试里的高频追问,考察点集中在三方面:

  1. 是否真正用过 PHP 8 的枚举(enum),而不仅仅是“知道”;
  2. 能否把枚举当成“类型安全”的常量集合,与 match 表达式做穷尽匹配;
  3. 是否理解 match 的“严格类型+返回值”特性,从而写出无 warning、无 default 也能通过静态分析的代码。
    面试官通常会让你现场写一段“订单状态流转”或“支付渠道路由”的代码,如果你用 switch+case 写,会被追问“为什么不用 match+enum”;如果你写了 match 却留下 default,会被追问“为什么不做穷尽匹配”。因此,答题时必须给出“无 default、不报错、可静态检测”的代码,并解释其底层原理。

知识点

  1. PHP 8.1 起支持两种枚举:
    • 纯枚举(pure enum):仅含 case 列表,无附加数据;
    • 回退枚举(backed enum):每个 case 绑定一个标量(string|int),可 from/tryFrom。
  2. 枚举自带 cases() 静态方法,返回全部实例数组,可用于单元测试或路由注册。
  3. match 表达式是 PHP 8 引入的“严格比较”分支结构,要求所有分支必须返回同一类型,且可静态分析穷尽性。
  4. 当 match 的 subject 为枚举实例时,如果分支列表未覆盖全部 case,IDE(PhpStorm)与静态工具(PHPStan level 8)会报“Unhandled cases”,这是国内大厂代码合并前的强制门禁。
  5. 若业务暂时无法覆盖全部枚举值,可用 @phpstan-ignore-next-line 或显式 default,但面试时优先展示“穷尽匹配”写法,体现对类型安全的极致追求。

答案

以下示例用“支付渠道”回退枚举演示 match 表达式的穷尽匹配,代码可直接通过 PHPStan level 8 与 Psalm 扫描,无 warning、无 default。

<?php
declare(strict_types=1);

enum PayChannel: string
{
    case WECHAT = 'wechat';
    case ALIPAY = 'alipay';
    case UNIONPAY = 'unionpay';

    /**
     * 根据渠道返回对应网关 URL
     */
    public function gateway(): string
    {
        return match ($this) {
            self::WECHAT  => 'https://api.mch.weixin.qq.com/pay/unifiedorder',
            self::ALIPAY  => 'https://openapi.alipay.com/gateway.do',
            self::UNIONPAY=> 'https://gateway.95516.com/gateway/api',
        };
    }

    /**
     * 返回中文名,用于前端下拉框
     */
    public function label(): string
    {
        return match ($this) {
            self::WECHAT   => '微信支付',
            self::ALIPAY   => '支付宝',
            self::UNIONPAY => '银联云闪付',
        };
    }
}

// 使用方
$channel = PayChannel::from($_GET['channel'] ?? 'wechat');
$url     = $channel->gateway();
$name    = $channel->label();

要点说明:

  1. match 的 subject 是 $this,即当前枚举实例;
  2. 三个 case 被全部列出,无需 default,后续新增 case 时,IDE 会强制你补全 match 分支,避免线上遗漏;
  3. 回退枚举的 from() 会在非法值时抛 ValueError,天然挡掉脏数据;
  4. 如果业务后续增加“APPLE_PAY”,只需在枚举中加 case,并在 match 两处方法内补分支即可,编译期就能发现问题,符合国内主流 CI 流程。

拓展思考

  1. 纯枚举如何与 match 结合?
    纯枚举没有回退值,常用于策略标识。例如权限角色:

    enum Role { case Admin; case Editor; case Guest; }
    
    function canDelete(Role $role): bool
    {
        return match ($role) {
            Role::Admin   => true,
            Role::Editor  => false,
            Role::Guest   => false,
        };
    }
    

    同样无需 default,PHPStan 可验证穷尽性。

  2. 如果枚举 case 很多,match 代码过长怎么办?
    国内大型 SaaS 的做法是:

    • 把“行为”封装成接口,用枚举实现接口,消除巨型 match;
    • 或者把 match 拆到专门的 Mapper 类,配合 array{case: callable} 映射表,保持 match 只有一行 return $map[$this->value]();,兼顾性能与可维护性。
  3. 性能对比:
    在 PHP 8.3 + OPcache 环境下,match+enum 与 switch+const 做 100 万次分支耗时几乎一致(±1%),但 match 省掉一次松散比较(==),且无隐式类型转换,字节码更短,实际 QPS 高并发场景下可略微降低 CPU 占用,国内电商大促压测数据已验证。