Skip to content

UnhandledMatchError、FiberError 与其他特殊错误

概述

PHP 8.0+ 引入了一些与现代语言特性相关的新错误类型。UnhandledMatchErrormatch 表达式配合使用,FiberError 与 PHP 8.1+ 引入的 Fiber 并发机制配合使用。此外,PHP 8.4+ 新增了 RequestParseBodyException 用于 HTTP 请求体解析场景。

PHP 版本说明

  • UnhandledMatchError:PHP 8.0+,继承自 Error
  • FiberError:PHP 8.1+,继承自 Error
  • RequestParseBodyException:PHP 8.4+,继承自 Exception

基础概念

UnhandledMatchError

match 表达式是 PHP 8.0 引入的新特性,类似于 switch 但使用严格比较(===)。当 match 表达式没有匹配到任何分支且没有 default 分支时,会抛出 UnhandledMatchError

php
<?php

declare(strict_types=1);

$status = 'pending';

// 有 default,不会抛出 UnhandledMatchError
$result = match ($status) {
    'active'  => '活跃',
    'deleted' => '已删除',
    default  => '未知状态',
};
echo $result; // "未知状态"

FiberError

Fiber 是 PHP 8.1 引入的协程/纤程机制,允许函数在中途暂停和恢复。当对 Fiber 执行不合法的操作(如重复启动、在错误状态恢复等)时,会抛出 FiberError

Error
├── TypeError
├── ValueError
├── UnhandledMatchError  ← PHP 8.0+
└── FiberError           ← PHP 8.1+

RequestParseBodyException

PHP 8.4+ 引入的 RequestParseBodyException,在 HTTP 请求体解析失败时由 PHP 引擎抛出,可通过自定义处理器捕获。

语法与代码

UnhandledMatchError 触发条件

php
<?php

declare(strict_types=1);

function getHttpStatusText(int $code): string
{
    return match ($code) {
        200 => 'OK',
        301 => 'Moved Permanently',
        404 => 'Not Found',
        500 => 'Internal Server Error',
        // 没有 default 分支
    };
}

// 传入未定义的状态码
try {
    $text = getHttpStatusText(403);
} catch (\UnhandledMatchError $e) {
    echo $e->getMessage();
    // "Unhandled match case 403"
}

match 与 switch 的错误处理差异

php
<?php

declare(strict_types=1);

// switch 没有匹配时不会报错,只是不执行任何分支
function switchExample(int $value): string
{
    $result = 'default';
    switch ($value) {
        case 1:
            $result = 'one';
            break;
        case 2:
            $result = 'two';
            break;
        // 没有匹配时,$result 保持 'default'
    }
    return $result;
}

echo switchExample(999); // "default" — 不报错

// match 没有匹配且无 default 时抛出 UnhandledMatchError
function matchExample(int $value): string
{
    return match ($value) {
        1 => 'one',
        2 => 'two',
        // 没有 default → UnhandledMatchError
    };
}

try {
    matchExample(999);
} catch (\UnhandledMatchError $e) {
    echo $e->getMessage(); // "Unhandled match case 999"
}

FiberError 常见触发场景

php
<?php

declare(strict_types=1);

$fiber = new \Fiber(function (): void {
    $value = \Fiber::suspend('第一次暂停');
    echo "收到值: {$value}";
});

// 1. 在未启动的 Fiber 上 resume
try {
    $fiber->resume('hello'); // Fiber 还没 start
} catch (\FiberError $e) {
    echo $e->getMessage(); // "Cannot resume a fiber that has not been started"
}

$fiber->start(); // 返回 '第一次暂停'
// Fiber 已结束(函数执行完毕),再次 resume
try {
    $fiber->resume('world');
} catch (\FiberError $e) {
    echo $e->getMessage(); // "Cannot resume a fiber that has already returned"
}

FiberError 完整状态机错误

php
<?php

declare(strict_types=1);

$fiber = new \Fiber(function (): void {
    \Fiber::suspend();
});

// 错误 1: 在 Fiber 内部调用 start
// (只能在主线程调用 start)
// 这会抛出 FiberError

// 错误 2: 对已启动但未暂停的 Fiber 调用 start
$fiber->start(); // 第一次启动成功
try {
    $fiber->start(); // 再次 start → FiberError
} catch (\FiberError $e) {
    echo '重复 start: ' . $e->getMessage();
}

// 错误 3: 在 Fiber 内部调用 resume
$fiber2 = new \Fiber(function (): void {
    // $fiber2->resume('value'); // FiberError: 不能在 Fiber 内 resume 自身
    \Fiber::suspend();
});

$fiber2->start();

// 错误 4: 对已终止的 Fiber 调用 resume
$fiber3 = new \Fiber(function (): void {
    echo "执行完毕";
    // 没有调用 suspend,自然结束
});

$fiber3->start();
try {
    $fiber3->resume();
} catch (\FiberError $e) {
    echo 'Fiber 已终止: ' . $e->getMessage();
}

PHP 8.4+ RequestParseBodyException

php
<?php

declare(strict_types=1);

// PHP 8.4+ 可以自定义请求体解析失败处理器
set_exception_handler(function (\Throwable $e): void {
    if ($e instanceof \RequestParseBodyException) {
        error_log("请求体解析失败: {$e->getMessage()}");
        http_response_code(400);
        echo json_encode(['error' => '无效的请求体']);
        return;
    }

    // 其他异常处理...
});

详细说明

Fiber 的生命周期与 FiberError 的关系

Fiber 有三种状态,每种状态只能接受特定的操作:

当前状态允许的操作非法操作 → 抛出 FiberError
未启动 (CREATED)start()resume(), throw()
已暂停 (SUSPENDED)resume(), throw()start()
已终止 (TERMINATED)start(), resume(), throw()
正在执行 (RUNNING)无(运行中)start(), resume(), throw()

UnhandledMatchError 的设计哲学

UnhandledMatchError 的引入体现了 PHP 对穷尽匹配(Exhaustive Matching)的追求:

  • switch:静默忽略未匹配的情况(不安全)
  • match + default:显式处理所有情况(安全)
  • matchdefault:编译器/运行时帮你检查是否遗漏(最安全)

设计意图

UnhandledMatchError 强制开发者处理所有可能的输入情况,避免因遗漏分支而导致的逻辑错误。这在处理枚举(Enum)类型时尤其有用。

php
<?php

declare(strict_types=1);

enum UserRole: string
{
    case Admin     = 'admin';
    case Editor    = 'editor';
    case Viewer    = 'viewer';
}

// 添加新的枚举值时,match 会自动报错提醒你补充处理逻辑
function getRoleLabel(UserRole $role): string
{
    return match ($role) {
        UserRole::Admin  => '管理员',
        UserRole::Editor => '编辑者',
        UserRole::Viewer => '查看者',
        // 如果将来添加 UserRole::SuperAdmin,
        // 而这里忘了处理 → UnhandledMatchError
    };
}

RequestParseBodyException 触发条件

场景说明
Content-Type 不匹配声明 application/json 但内容格式错误
请求体过大超过 post_max_sizeupload_max_filesize
编码问题请求体包含非法的字符编码
MALFORMED 头部multipart 边界符错误

实战示例

安全的 match 枚举处理

php
<?php

declare(strict_types=1);

enum PaymentStatus: string
{
    case Pending   = 'pending';
    case Completed = 'completed';
    case Failed    = 'failed';
    case Refunded  = 'refunded';
}

function handlePaymentStatus(PaymentStatus $status): string
{
    // 使用 default 作为安全网,但不忽略具体值
    return match ($status) {
        PaymentStatus::Pending   => '等待处理',
        PaymentStatus::Completed => '支付成功',
        PaymentStatus::Failed    => '支付失败,请重试',
        PaymentStatus::Refunded  => '已退款',
    };
}

// 或者捕获 UnhandledMatchError 提供更好的错误信息
function handlePaymentStatusSafe(PaymentStatus $status): string
{
    try {
        return match ($status) {
            PaymentStatus::Pending   => '等待处理',
            PaymentStatus::Completed => '支付成功',
            PaymentStatus::Failed    => '支付失败',
            PaymentStatus::Refunded  => '已退款',
        };
    } catch (\UnhandledMatchError $e) {
        error_log("未处理的支付状态: {$e->getMessage()}");
        return '状态异常';
    }
}

Fiber 池管理器

php
<?php

declare(strict_types=1);

class FiberPool
{
    /** @var \Fiber[] */
    private array $fibers = [];

    public function add(callable $callback): \Fiber
    {
        $fiber = new \Fiber($callback);
        $this->fibers[] = $fiber;
        return $fiber;
    }

    public function startAll(): void
    {
        foreach ($this->fibers as $fiber) {
            try {
                $fiber->start();
            } catch (\FiberError $e) {
                echo "Fiber 启动失败: {$e->getMessage()}" . PHP_EOL;
            } catch (\Throwable $e) {
                echo "Fiber 内部异常: {$e->getMessage()}" . PHP_EOL;
            }
        }
    }

    public function resumeAll(mixed $value = null): void
    {
        foreach ($this->fibers as $fiber) {
            try {
                $fiber->resume($value);
            } catch (\FiberError $e) {
                // Fiber 可能已终止,忽略
                continue;
            }
        }
    }
}

// 使用示例
$pool = new FiberPool();

$pool->add(function (): void {
    echo "任务 1 开始" . PHP_EOL;
    \Fiber::suspend();
    echo "任务 1 恢复" . PHP_EOL;
});

$pool->add(function (): void {
    echo "任务 2 开始" . PHP_EOL;
    \Fiber::suspend();
    echo "任务 2 恢复" . PHP_EOL;
});

$pool->startAll();
echo "--- 所有任务暂停 ---" . PHP_EOL;
$pool->resumeAll();
echo "--- 所有任务恢复 ---" . PHP_EOL;

注意事项

  1. UnhandledMatchError 只在运行时触发:PHP 不会在编译期静态检查 match 是否穷尽了所有可能的值(不像 Rust 的 match)。

  2. match 使用严格比较match 使用 === 而非 ==,所以 match (0) 不会匹配 false 分支。

  3. Fiber 不能嵌套 start:不能在一个 Fiber 内部 start 另一个 Fiber 并等待其结果——可以使用 Fiber::suspend() 配合 resume() 实现协程调度。

  4. FiberError 不是 ExceptionFiberError 继承自 Error,不是 Exception。如果只 catch (\Exception $e),不会捕获到 FiberError

  5. RequestParseBodyException 是 Exception:与 FiberErrorUnhandledMatchError 不同,RequestParseBodyException 继承自 Exception 而非 Error

最佳实践

推荐做法

  1. match 处理枚举时省略 default:当 match 的目标是一个有限的枚举类型时,省略 defaultUnhandledMatchError 帮你发现遗漏
  2. match 处理动态值时添加 default:当匹配目标来自用户输入等不确定来源时,提供 default 分支
  3. Fiber 操作包裹在 try/catch 中:Fiber 状态管理复杂,应始终捕获 FiberError
  4. 使用 Fiber 前检查状态:调用操作前通过 Fiber::getStatus() 检查当前状态

参考链接