UnhandledMatchError、FiberError 与其他特殊错误
概述
PHP 8.0+ 引入了一些与现代语言特性相关的新错误类型。UnhandledMatchError 与 match 表达式配合使用,FiberError 与 PHP 8.1+ 引入的 Fiber 并发机制配合使用。此外,PHP 8.4+ 新增了 RequestParseBodyException 用于 HTTP 请求体解析场景。
PHP 版本说明
UnhandledMatchError:PHP 8.0+,继承自ErrorFiberError:PHP 8.1+,继承自ErrorRequestParseBodyException:PHP 8.4+,继承自Exception
基础概念
UnhandledMatchError
match 表达式是 PHP 8.0 引入的新特性,类似于 switch 但使用严格比较(===)。当 match 表达式没有匹配到任何分支且没有 default 分支时,会抛出 UnhandledMatchError。
<?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
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
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
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
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
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:显式处理所有情况(安全)match无default:编译器/运行时帮你检查是否遗漏(最安全)
设计意图
UnhandledMatchError 强制开发者处理所有可能的输入情况,避免因遗漏分支而导致的逻辑错误。这在处理枚举(Enum)类型时尤其有用。
<?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_size 或 upload_max_filesize |
| 编码问题 | 请求体包含非法的字符编码 |
| MALFORMED 头部 | multipart 边界符错误 |
实战示例
安全的 match 枚举处理
<?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
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;注意事项
UnhandledMatchError 只在运行时触发:PHP 不会在编译期静态检查
match是否穷尽了所有可能的值(不像 Rust 的 match)。match 使用严格比较:
match使用===而非==,所以match (0)不会匹配false分支。Fiber 不能嵌套 start:不能在一个 Fiber 内部
start另一个 Fiber 并等待其结果——可以使用Fiber::suspend()配合resume()实现协程调度。FiberError 不是 Exception:
FiberError继承自Error,不是Exception。如果只catch (\Exception $e),不会捕获到FiberError。RequestParseBodyException 是 Exception:与
FiberError和UnhandledMatchError不同,RequestParseBodyException继承自Exception而非Error。
最佳实践
推荐做法
- match 处理枚举时省略 default:当
match的目标是一个有限的枚举类型时,省略default让UnhandledMatchError帮你发现遗漏 - match 处理动态值时添加 default:当匹配目标来自用户输入等不确定来源时,提供
default分支 - Fiber 操作包裹在 try/catch 中:Fiber 状态管理复杂,应始终捕获
FiberError - 使用 Fiber 前检查状态:调用操作前通过
Fiber::getStatus()检查当前状态