在PHP开发中,错误处理是一个非常重要的话题。任何程序都可能出现错误,无论是语法错误、运行时错误、逻辑错误,还是外部资源错误(如数据库连接失败、文件不存在)。良好的错误处理,能让程序在出现错误时,给出友好的提示,记录详细的错误信息,方便调试和排查问题,而不是直接崩溃或显示空白页面。

PHP提供了丰富的错误处理机制,包括错误报告、错误日志、自定义错误处理、异常处理等。掌握这些机制,能帮你写出更健壮、更易维护的PHP代码。

今天分享PHP错误处理和异常的详解,从基础到进阶,帮你掌握PHP的错误处理机制。

PHP错误类型

PHP的错误分为多种类型,不同类型的错误,严重程度不同,处理方式也不同。

错误级别常量

  • E_ERROR:致命错误,程序无法继续执行,如调用不存在的函数、内存耗尽
  • E_WARNING:警告,程序可以继续执行,但可能有问题,如包含不存在的文件、除以0
  • E_PARSE:解析错误,语法错误,程序无法执行
  • E_NOTICE:通知,可能是错误,也可能是正常的,如使用未定义的变量
  • ECOREERROR:PHP内核启动时的致命错误
  • ECOREWARNING:PHP内核启动时的警告
  • ECOMPILEERROR:编译时的致命错误
  • ECOMPILEWARNING:编译时的警告
  • EUSERERROR:用户触发的致命错误
  • EUSERWARNING:用户触发的警告
  • EUSERNOTICE:用户触发的通知
  • E_STRICT:严格模式,建议改进代码以保证最佳互操作性和向前兼容性
  • ERECOVERABLEERROR:可捕获的致命错误,如类型提示错误
  • E_DEPRECATED:废弃警告,使用了即将被移除的功能
  • EUSERDEPRECATED:用户触发的废弃警告

错误级别配置

可以在php.ini中配置错误报告级别:

; 显示所有错误
error_reporting = E_ALL

; 显示所有错误,除了通知
error_reporting = E_ALL & ~E_NOTICE

; 显示所有错误,除了通知和废弃警告
error_reporting = E_ALL & ~E_NOTICE & ~E_DEPRECATED

也可以在代码中动态配置:

// 显示所有错误
error_reporting(E_ALL);

// 显示所有错误,除了通知
error_reporting(E_ALL & ~E_NOTICE);

// 关闭所有错误显示
error_reporting(0);

错误显示和日志

错误显示

; 是否显示错误
display_errors = On

; 是否显示启动错误
display_startup_errors = On

开发环境建议开启错误显示,方便调试;生产环境建议关闭错误显示,避免泄露服务器信息,把错误记录到日志。

错误日志

; 是否记录错误日志
log_errors = On

; 错误日志文件路径
error_log = /var/log/php_errors.log

; 日志最大长度
log_errors_max_len = 1024

也可以在代码中用error_log()函数记录错误:

error_log('这是一条错误日志');
error_log('这是一条错误日志', 3, '/path/to/error.log');

基本错误处理

1. 错误抑制符@

在表达式前加@,可以抑制该表达式的错误:

$content = @file_get_contents('not_exist.txt');

注意:@会抑制所有错误,包括致命错误,不建议滥用,只在确定可以忽略错误时使用。

2. 条件判断

对可能出错的操作,用条件判断:

$file = 'test.txt';
if (file_exists($file)) {
    $content = file_get_contents($file);
} else {
    echo '文件不存在';
}
$conn = mysqli_connect('localhost', 'user', 'pass', 'db');
if (!$conn) {
    die('数据库连接失败:' . mysqli_connect_error());
}

3. trigger_error触发用户错误

if ($age < 0) {
    trigger_error('年龄不能为负数', E_USER_WARNING);
}

自定义错误处理函数

PHP允许用seterrorhandler()设置自定义错误处理函数,接管PHP的错误处理:

function myErrorHandler($errno, $errstr, $errfile, $errline) {
    $errorTypes = [
        E_ERROR => '致命错误',
        E_WARNING => '警告',
        E_PARSE => '解析错误',
        E_NOTICE => '通知',
        E_USER_ERROR => '用户致命错误',
        E_USER_WARNING => '用户警告',
        E_USER_NOTICE => '用户通知',
        E_STRICT => '严格模式',
        E_RECOVERABLE_ERROR => '可捕获错误',
        E_DEPRECATED => '废弃警告',
        E_USER_DEPRECATED => '用户废弃警告',
    ];
    
    $type = isset($errorTypes[$errno]) ? $errorTypes[$errno] : '未知错误';
    
    // 记录错误日志
    error_log("[$type] $errstr in $errfile on line $errline");
    
    // 开发环境显示错误
    if (ini_get('display_errors')) {
        echo "<b>$type</b>: $errstr in <b>$errfile</b> on line <b>$errline</b><br>";
    }
    
    // 返回true,表示错误已处理,不再执行PHP内部错误处理
    return true;
}

// 设置自定义错误处理
set_error_handler('myErrorHandler');

注意:自定义错误处理函数只能处理以下错误类型:EWARNING、ENOTICE、EUSER*、EDEPRECATED、ESTRICT、ERECOVERABLEERROR。EERROR、EPARSE等致命错误无法被自定义错误处理函数捕获,因为这些错误发生后程序会立即终止。

可以用restoreerrorhandler()恢复默认错误处理。

异常处理(Exception)

PHP 5引入了异常处理机制,和Java、C#等语言的异常处理类似。异常处理是面向对象的错误处理方式,更灵活、更强大。

1. 抛出异常

throw关键字抛出异常:

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

2. 捕获异常

try-catch捕获异常:

try {
    $result = divide(10, 0);
    echo $result;
} catch (Exception $e) {
    echo '捕获异常:' . $e->getMessage();
    echo '异常文件:' . $e->getFile();
    echo '异常行号:' . $e->getLine();
    echo '异常追踪:' . $e->getTraceAsString();
}

3. finally块

finally块中的代码,无论是否发生异常,都会执行:

try {
    $result = divide(10, 0);
    echo $result;
} catch (Exception $e) {
    echo '捕获异常:' . $e->getMessage();
} finally {
    echo 'finally块总是执行';
    // 通常用来清理资源,如关闭数据库连接、文件句柄等
}

4. 多个catch块

可以捕获不同类型的异常:

try {
    // 代码
} catch (InvalidArgumentException $e) {
    echo '参数错误:' . $e->getMessage();
} catch (RuntimeException $e) {
    echo '运行时错误:' . $e->getMessage();
} catch (Exception $e) {
    echo '其他错误:' . $e->getMessage();
}

注意:catch块的顺序很重要,子类异常要放在父类异常前面,否则父类异常会先被子类捕获。

5. 自定义异常类

可以继承Exception类,创建自定义异常:

class DatabaseException extends Exception {
    public function __construct($message, $code = 0, Exception $previous = null) {
        parent::__construct($message, $code, $previous);
    }
    
    public function __toString() {
        return __CLASS__ . ": [{$this->code}]: {$this->message}\n";
    }
}

class FileException extends Exception {}

// 使用自定义异常
try {
    throw new DatabaseException('数据库连接失败');
} catch (DatabaseException $e) {
    echo '数据库错误:' . $e->getMessage();
} catch (Exception $e) {
    echo '其他错误:' . $e->getMessage();
}

6. 异常方法

Exception类提供了以下方法:

  • getMessage():异常消息
  • getCode():异常代码
  • getFile():异常发生的文件
  • getLine():异常发生的行号
  • getTrace():异常追踪(数组)
  • getTraceAsString():异常追踪(字符串)
  • getPrevious():前一个异常(异常链)
  • __toString():异常的字符串表示

错误和异常的区别

  • 错误(Error):PHP的传统错误处理机制,主要用于PHP内部错误,如语法错误、运行时错误。错误有不同的级别(EERROR、EWARNING、ENOTICE等),可以用seterror_handler()自定义处理。
  • 异常(Exception):面向对象的错误处理机制,主要用于业务逻辑错误,如参数错误、业务规则违反。异常可以用try-catch捕获,可以自定义异常类,可以有异常链。

在PHP 7中,引入了Error类,实现了Throwable接口,Error和Exception都实现了Throwable接口。所以在PHP 7中,可以用try-catch捕获Error:

try {
    // 可能发生致命错误的代码
} catch (Error $e) {
    echo '捕获错误:' . $e->getMessage();
} catch (Exception $e) {
    echo '捕获异常:' . $e->getMessage();
}

也可以用Throwable捕获所有错误和异常:

try {
    // 代码
} catch (Throwable $e) {
    echo '捕获到:' . $e->getMessage();
}

实战:数据库操作的错误处理

下面是一个数据库操作的错误处理示例:

class Database {
    private $pdo;
    
    public function __construct($host, $dbname, $user, $pass) {
        try {
            $this->pdo = new PDO(
                "mysql:host=$host;dbname=$dbname;charset=utf8mb4",
                $user,
                $pass,
                [
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                    PDO::ATTR_EMULATE_PREPARES => false,
                ]
            );
        } catch (PDOException $e) {
            // 记录错误日志
            error_log('数据库连接失败:' . $e->getMessage());
            // 抛出自定义异常
            throw new DatabaseException('数据库连接失败,请稍后重试');
        }
    }
    
    public function query($sql, $params = []) {
        try {
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute($params);
            return $stmt;
        } catch (PDOException $e) {
            error_log('数据库查询失败:' . $e->getMessage() . ' SQL: ' . $sql);
            throw new DatabaseException('数据库查询失败');
        }
    }
    
    public function fetchAll($sql, $params = []) {
        return $this->query($sql, $params)->fetchAll();
    }
    
    public function fetchOne($sql, $params = []) {
        return $this->query($sql, $params)->fetch();
    }
}

// 使用
try {
    $db = new Database('localhost', 'test', 'user', 'pass');
    $users = $db->fetchAll('SELECT * FROM users');
    print_r($users);
} catch (DatabaseException $e) {
    echo '错误:' . $e->getMessage();
} catch (Exception $e) {
    echo '未知错误:' . $e->getMessage();
}

这个示例中,数据库操作的错误都被捕获,记录详细日志,然后抛出友好的自定义异常,调用方捕获异常并给出友好提示。

错误处理最佳实践

  1. 开发环境显示所有错误:开发环境开启displayerrors = Onerrorreporting = E_ALL,方便调试。
  2. 生产环境关闭错误显示,记录日志:生产环境关闭displayerrors,开启logerrors,把错误记录到日志,避免泄露服务器信息。
  3. 使用异常处理业务逻辑错误:业务逻辑错误(如参数错误、权限不足、业务规则违反)用异常处理,不要用普通的错误。
  4. 捕获异常后要处理:捕获异常后,要么处理(记录日志、给出提示、重试),要么重新抛出,不要空捕获(catch了什么都不做)。
  5. 不要捕获所有异常:只捕获你能处理的异常,不要用catch (Exception $e)捕获所有异常然后忽略。
  6. 使用finally清理资源:需要清理的资源(数据库连接、文件句柄、锁等)放在finally块中,确保无论是否发生异常都能清理。
  7. 自定义异常类:为不同类型的错误创建自定义异常类,方便区分和处理。
  8. 异常消息要友好:给用户看的异常消息要友好,不要泄露技术细节;技术细节记录到日志。
  9. 不要用@抑制错误:除非确定可以忽略,否则不要用@抑制错误,这会隐藏问题。
  10. 记录详细的错误日志:错误日志要包含错误类型、消息、文件、行号、堆栈追踪、时间、用户IP等,方便排查问题。
  11. 设置全局异常处理:用setexceptionhandler()设置全局异常处理,捕获未被catch的异常,给出友好的错误页面。
  12. 错误页面要友好:生产环境出错时,显示友好的错误页面,不要显示PHP的错误信息。

全局异常处理

setexceptionhandler()设置全局异常处理:

function myExceptionHandler($e) {
    // 记录错误日志
    error_log('未捕获异常:' . $e->getMessage() . "\n" . $e->getTraceAsString());
    
    // 开发环境显示详细错误
    if (ini_get('display_errors')) {
        echo '<h1>发生异常</h1>';
        echo '<p>' . get_class($e) . ': ' . $e->getMessage() . '</p>';
        echo '<p>文件:' . $e->getFile() . ' 行号:' . $e->getLine() . '</p>';
        echo '<pre>' . $e->getTraceAsString() . '</pre>';
    } else {
        // 生产环境显示友好错误页面
        http_response_code(500);
        echo '<h1>服务器内部错误</h1>';
        echo '<p>抱歉,服务器发生了内部错误,请稍后重试。</p>';
    }
}

set_exception_handler('myExceptionHandler');

常见问题

1. 为什么显示空白页面

原因:发生了致命错误,但displayerrors关闭了,所以显示空白。 解决:开发环境开启displayerrors = On,或者查看错误日志。

2. 为什么自定义错误处理函数不生效

原因:自定义错误处理函数只能处理部分错误类型,EERROR、EPARSE等致命错误无法被捕获。 解决:致命错误无法用自定义错误处理函数捕获,需要在php.ini中配置错误显示和日志。PHP 7中可以用try-catch捕获Error。

3. 异常和错误用哪个

建议:

  • PHP内部错误(语法错误、运行时错误)用错误处理
  • 业务逻辑错误(参数错误、权限不足、业务规则违反)用异常处理
  • 新代码推荐用异常处理,更灵活、更面向对象

4. 如何调试错误

  • 开发环境开启错误显示,看到详细错误信息
  • 查看错误日志,找到错误发生的文件和行号
  • vardump()printr()输出变量,调试逻辑
  • 用Xdebug等调试工具,断点调试
  • debug_backtrace()查看函数调用栈

总结

PHP错误处理,是写出健壮代码的基础。

核心要点:

  1. 错误类型:EERROR、EWARNING、ENOTICE、EPARSE等,不同级别严重程度不同
  2. 错误配置:errorreporting、displayerrors、logerrors、errorlog
  3. 基本错误处理:@错误抑制符、条件判断、trigger_error
  4. 自定义错误处理:seterrorhandler,接管PHP错误处理
  5. 异常处理:throw抛出、try-catch捕获、finally清理、自定义异常类
  6. 错误和异常的区别:错误用于PHP内部错误,异常用于业务逻辑错误;PHP 7中Error也可以被catch
  7. 实战:数据库操作的错误处理,捕获异常、记录日志、抛出友好异常
  8. 最佳实践:开发显示错误、生产记录日志、用异常处理业务错误、捕获后要处理、finally清理资源、自定义异常、友好错误消息、详细日志、全局异常处理、友好错误页面
  9. 全局异常处理:setexceptionhandler,捕获未被catch的异常

良好的错误处理,能让你的程序在出现错误时,不崩溃、不泄露信息、给出友好提示、记录详细日志,方便排查问题。作为PHP开发者,一定要重视错误处理,写出健壮、易维护的代码。