做Web开发,错误处理是一个很重要但经常被忽视的话题。

很多PHP开发者,尤其是初学者,不重视错误处理。代码写得很随意,不检查返回值,不捕获异常,不处理错误。结果就是:程序一出错,就直接白屏,或者显示一大段错误信息,把数据库密码、文件路径等敏感信息都暴露出来。用户体验很差,也有安全隐患。

一个健壮的程序,应该能优雅地处理各种错误和异常——出错了不崩溃,给用户友好的提示,同时记录详细的错误日志,方便开发者排查问题。

PHP提供了完善的错误处理机制,包括错误报告、自定义错误处理、异常处理(try-catch)等。今天分享PHP错误处理的最佳实践,帮你写出更健壮、更友好的代码。

PHP的错误类型

PHP的错误分为多种类型,常见的:

  • E_ERROR:致命错误,程序无法继续执行(如调用不存在的函数、内存耗尽)
  • E_WARNING:警告,非致命错误,程序继续执行(如include不存在的文件)
  • E_NOTICE:通知,可能是bug也可能不是(如使用未定义的变量)
  • E_PARSE:解析错误,语法错误,代码无法执行
  • E_DEPRECATED:废弃警告,使用了即将废弃的功能
  • E_STRICT:严格标准,代码不符合最佳实践
  • ERECOVERABLEERROR:可捕获的致命错误

开发环境应该显示所有错误,方便调试;生产环境应该隐藏错误,只记录日志。

错误报告配置

php.ini配置:

; 开发环境:显示所有错误
error_reporting = E_ALL
display_errors = On
display_startup_errors = On
log_errors = On
error_log = /var/log/php_errors.log

; 生产环境:隐藏错误,只记录日志
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
display_errors = Off
log_errors = On
error_log = /var/log/php_errors.log

代码中动态配置:

// 开发环境
ini_set('display_errors', '1');
error_reporting(E_ALL);

// 生产环境
ini_set('display_errors', '0');
error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);

注意:生产环境一定要关闭display_errors,避免暴露敏感信息。

自定义错误处理

PHP允许用seterrorhandler()自定义错误处理函数,将错误转换为异常或记录日志:

function customErrorHandler($errno, $errstr, $errfile, $errline) {
    // 如果错误被@抑制,不处理
    if (!(error_reporting() & $errno)) {
        return false;
    }

    $errorTypes = [
        E_ERROR => 'E_ERROR',
        E_WARNING => 'E_WARNING',
        E_NOTICE => 'E_NOTICE',
        E_PARSE => 'E_PARSE',
        E_DEPRECATED => 'E_DEPRECATED',
    ];

    $type = isset($errorTypes[$errno]) ? $errorTypes[$errno] : 'UNKNOWN';
    $message = "[$type] $errstr in $errfile on line $errline";

    // 记录日志
    error_log($message);

    // 开发环境抛出异常,方便调试
    if (ini_get('display_errors')) {
        throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
    }

    return true; // 阻止PHP默认错误处理
}

set_error_handler('customErrorHandler');

异常处理(try-catch)

PHP 5引入了异常处理机制,用try-catch捕获和处理异常:

try {
    // 可能抛出异常的代码
    $result = divide(10, 0);
    echo $result;
} catch (InvalidArgumentException $e) {
    // 捕获特定类型的异常
    echo "参数错误:" . $e->getMessage();
} catch (Exception $e) {
    // 捕获所有异常
    echo "发生错误:" . $e->getMessage();
} finally {
    // 无论是否发生异常,都会执行(可选)
    echo "执行完毕";
}

抛出异常:

function divide($a, $b) {
    if ($b == 0) {
        throw new InvalidArgumentException('除数不能为0');
    }
    return $a / $b;
}

异常的常用方法:

  • $e->getMessage():异常消息
  • $e->getCode():异常代码
  • $e->getFile():抛出异常的文件
  • $e->getLine():抛出异常的行号
  • $e->getTrace():堆栈跟踪(数组)
  • $e->getTraceAsString():堆栈跟踪(字符串)

异常分类

PHP的异常体系:

  • Throwable:所有错误和异常的接口(PHP 7+)

- Error:PHP内部错误(PHP 7+) - TypeError:类型错误 - ParseError:解析错误 - ArithmeticError:算术错误 - DivisionByZeroError:除零错误 - Exception:用户级异常 - RuntimeException:运行时异常 - LogicException:逻辑异常 - InvalidArgumentException:无效参数异常 - PDOException:数据库异常 - ...

自定义异常:

class MyAppException extends Exception {}
class DatabaseException extends MyAppException {}
class ValidationException extends MyAppException {}

// 使用
try {
    // ...
} catch (ValidationException $e) {
    // 验证错误,返回400
} catch (DatabaseException $e) {
    // 数据库错误,记录日志,返回500
} catch (Exception $e) {
    // 其他错误
}

全局异常捕获

在应用入口处设置全局异常处理器,捕获未处理的异常:

// 全局异常处理器
function exceptionHandler($e) {
    // 记录详细错误日志
    error_log("Uncaught exception: " . $e->getMessage() . "\n" . $e->getTraceAsString());

    // 开发环境显示详细错误
    if (ini_get('display_errors')) {
        echo "<h1>发生错误</h1>";
        echo "<p><strong>消息:</strong>" . $e->getMessage() . "</p>";
        echo "<p><strong>文件:</strong>" . $e->getFile() . "</p>";
        echo "<p><strong>行号:</strong>" . $e->getLine() . "</p>";
        echo "<pre>" . $e->getTraceAsString() . "</pre>";
    } else {
        // 生产环境显示友好错误页面
        http_response_code(500);
        include 'error_500.php';
    }
}

set_exception_handler('exceptionHandler');

错误日志

记录详细的错误日志,方便排查问题:

// 简单记录
error_log("Error: " . $message);

// 记录到指定文件
error_log("Error: " . $message, 3, '/var/log/myapp_errors.log');

// 发送邮件(严重错误)
error_log("Critical error: " . $message, 1, 'admin@example.com');

日志应该包含:

  • 错误时间
  • 错误类型和消息
  • 错误文件和行号
  • 堆栈跟踪
  • 请求URL和方法
  • 用户IP和User-Agent
  • 相关的请求参数(敏感信息脱敏)

友好的错误页面

生产环境出错时,不要显示技术细节,要显示友好的错误页面:

404页面(error_404.php):

<!DOCTYPE html>
<html>
<head><title>404 - 页面不存在</title></head>
<body>
    <h1>404</h1>
    <p>抱歉,您访问的页面不存在。</p>
    <a href="/">返回首页</a>
</body>
</html>

500页面(error_500.php):

<!DOCTYPE html>
<html>
<head><title>500 - 服务器错误</title></head>
<body>
    <h1>500</h1>
    <p>抱歉,服务器出了点问题,请稍后再试。</p>
    <p>问题已记录,我们会尽快修复。</p>
    <a href="/">返回首页</a>
</body>
</html>

常见错误处理场景

1. 数据库操作:

try {
    $pdo = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
    $stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
    $stmt->execute([$id]);
    $user = $stmt->fetch();
} catch (PDOException $e) {
    error_log("Database error: " . $e->getMessage());
    // 返回友好错误
    http_response_code(500);
    echo "数据库连接失败,请稍后再试";
}

2. 文件操作:

$file = '/path/to/file.txt';
if (!file_exists($file)) {
    throw new RuntimeException("文件不存在: $file");
}
if (!is_readable($file)) {
    throw new RuntimeException("文件不可读: $file");
}
$content = file_get_contents($file);
if ($content === false) {
    throw new RuntimeException("读取文件失败: $file");
}

3. 外部API调用:

try {
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = curl_error($ch);
    curl_close($ch);

    if ($error) {
        throw new RuntimeException("API请求失败: $error");
    }
    if ($httpCode >= 400) {
        throw new RuntimeException("API返回错误: HTTP $httpCode");
    }
    $data = json_decode($response, true);
    if ($data === null) {
        throw new RuntimeException("API返回数据解析失败");
    }
} catch (Exception $e) {
    error_log("API error: " . $e->getMessage());
    // 使用缓存数据或返回默认值
}

4. 用户输入验证:

function validateEmail($email) {
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        throw new InvalidArgumentException("邮箱格式不正确: $email");
    }
    return true;
}

try {
    validateEmail($_POST['email']);
} catch (InvalidArgumentException $e) {
    // 返回表单错误提示
    echo $e->getMessage();
}

错误处理最佳实践

  1. 开发环境显示所有错误,生产环境隐藏错误:方便调试,同时保证生产安全
  2. 永远不要忽略错误:不要用@抑制错误,不要空的catch块
  3. 检查返回值:函数调用后检查返回值,特别是数据库、文件、网络操作
  4. 使用异常处理错误:用try-catch捕获异常,不要用返回错误码的方式(PHP风格)
  5. 记录详细日志:出错时记录详细信息,方便排查
  6. 给用户友好提示:不要把技术细节暴露给用户
  7. 全局异常捕获:在应用入口设置全局异常处理器,兜底处理未捕获的异常
  8. 合理的错误分类:区分用户错误(400)、未授权(401)、禁止访问(403)、未找到(404)、服务器错误(500)
  9. 不要在catch里什么都不做:至少记录日志,或者重新抛出异常
  10. finally块清理资源:关闭数据库连接、文件句柄等,用finally保证执行

总结

错误处理是写出健壮代码的关键。一个好的错误处理机制,应该:

  • 开发环境显示详细错误,方便调试
  • 生产环境隐藏错误,只记录日志
  • 用try-catch捕获和处理异常
  • 记录详细的错误日志
  • 给用户友好的错误提示
  • 全局兜底,避免白屏

不要忽视错误处理,它是代码质量的重要组成部分。从今天开始,给你的代码加上完善的错误处理吧。