PHP 错误与异常处理

AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。

PHP 8.4

PHP 有两套机制:**错误(error)**用于引擎层面的问题,**异常(exception)**用于代码主动抛出的问题。分清两者,才能决定该捕获还是该修复。

错误等级

常见等级(PHP 8):

等级 场景 是否中断
E_ERROR 致命错误,如调用未定义函数 是
E_WARNING 运行时警告,如 include 失败 否
E_NOTICE 提示,如访问未定义变量或数组键 否
E_DEPRECATED 使用了将来会移除的特性 否
E_PARSE 语法错误 是

PHP 8 把很多历史错误升级成了 Error 异常(如 TypeError、DivisionByZeroError),所以现在大部分问题都能用 try / catch 处理。

异常体系

Throwable
├── Exception          (常规异常,业务代码抛这个)
│   ├── RuntimeException
│   ├── LogicException
│   └── ...
└── Error              (引擎级错误,PHP 7+)
    ├── TypeError
    ├── ValueError
    ├── DivisionByZeroError
    └── ParseError

捕获时用 Throwable 能同时覆盖两者;但业务代码通常只应捕获 Exception,引擎级 Error 一般代表代码有 bug,让它暴露出来更好修。

try / catch / finally

try {
    $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
    error_log("解析失败:" . $e->getMessage());
    $data = [];
} finally {
    // 总会执行,用于清理:关闭句柄、恢复状态
}

finally 无论是否发生异常、是否在 catch 里 return,都会执行。

多个 catch 从具体到宽泛排列:

try {
    process($input);
} catch (InvalidArgumentException $e) {
    echo "参数不合法:" . $e->getMessage();
} catch (RuntimeException $e) {
    echo "运行时出错:" . $e->getMessage();
} catch (Throwable $e) {
    error_log($e);
    echo "服务器内部错误";
}

Exception 的常用方法

try {
    throw new RuntimeException("连接超时");
} catch (RuntimeException $e) {
    $e->getMessage();   // "连接超时"
    $e->getCode();      // 错误码,默认 0
    $e->getFile();      // 抛出位置的文件
    $e->getLine();      // 抛出的行号
    $e->getTraceAsString();   // 调用栈
}

抛异常

function withdraw(float $balance, float $amount): float
{
    if ($amount <= 0) {
        throw new InvalidArgumentException("取款金额必须大于 0");
    }
    if ($amount > $balance) {
        throw new RuntimeException("余额不足", 1001);
    }
    return $balance - $amount;
}

选哪个异常类:

  • InvalidArgumentException:调用方传参错误(属于 LogicException,是代码 bug)。
  • RuntimeException:运行时外部条件不满足(余额不足、文件被删、网络超时)。
  • DomainException:值不在业务允许的范围内。

自定义异常

业务复杂时用自定义类型区分错误,比读 message 字符串可靠:

class ValidationException extends RuntimeException
{
    /** @param array<string, string> $errors 字段 => 错误信息 */
    public function __construct(public array $errors)
    {
        parent::__construct("输入校验未通过");
    }
}

try {
    $errors = validate($_POST);
    if ($errors !== []) {
        throw new ValidationException($errors);
    }
} catch (ValidationException $e) {
    foreach ($e->errors as $field => $message) {
        echo htmlspecialchars($field, ENT_QUOTES, "UTF-8") . ":" 
           . htmlspecialchars($message, ENT_QUOTES, "UTF-8") . "<br>";
    }
}

PHP 8 的构造器属性提升(public array $errors)省掉了属性声明与赋值。

让 PHP 函数抛异常而不是返回 false

很多老函数失败时返回 false,配合 JSON_THROW_ON_ERROR 这类标志可以改成抛异常:

// 默认:失败返回 null,需要 json_last_error() 查原因
$data = json_decode($raw, true);

// PHP 7.3+:失败直接抛 JsonException
$data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);

类似的还有 PDO::ERRMODE_EXCEPTION(数据库)、filter_var 仍需自行判断。

全局处理器:兜底最后一道

未被捕获的异常会导致致命错误,注册处理器可以做统一日志与友好页面:

set_exception_handler(function (Throwable $e): void {
    error_log(sprintf(
        "[%s] %s in %s:%d\n%s",
        date("c"),
        $e->getMessage(),
        $e->getFile(),
        $e->getLine(),
        $e->getTraceAsString()
    ));

    http_response_code(500);
    echo "服务器开小差了,请稍后再试";
});

放在应用入口(如 index.php)的最前面。注意它只处理未被捕获的异常。

错误报告配置

开发环境:显示全部错误。

; php.ini 开发环境
display_errors = On
error_reporting = E_ALL
log_errors = On

生产环境:记录到日志,不显示给用户(错误信息里可能含路径、SQL、凭据)。

; php.ini 生产环境
display_errors = Off
error_reporting = E_ALL
log_errors = On
error_log = /var/log/php/error.log

也可以在脚本里临时设置:

error_reporting(E_ALL);
ini_set("display_errors", "0");

常见错误与注意事项

  1. 空的 catch (Exception $e) {}:吞掉异常,问题被隐藏。至少要 error_log。
  2. 捕获 Throwable 后一律返回默认值:会把引擎级 bug 也一起掩盖,只捕获你预期的类型。
  3. 用 @ 抑制错误:错误信息消失,但代码仍在错误状态下继续运行。
  4. 生产环境开着 display_errors:泄露路径、SQL 和配置信息。
  5. 在 finally 里 return:会覆盖 try / catch 的返回值,几乎都不是本意。
  6. 异常消息直接输出给用户:应记录到日志,给用户通用提示。

小结

  • Exception 用于业务问题,Error 用于引擎级问题,Throwable 是两者的父接口。
  • 抛异常时选对类型:InvalidArgumentException(参数)vs RuntimeException(外部条件)。
  • json_decode 等函数可用 JSON_THROW_ON_ERROR 改成抛异常。
  • 入口注册 set_exception_handler 做统一兜底。
  • 生产环境 display_errors = Off + log_errors = On。

下一步