PHP 错误(Error) / 异常(Exception) 的捕获处理

经验分享 261 浏览

“错误”和“异常”的概念十分相似,都说明我们的系统出了问题,也都会给出错误信息和错误类型。但是“异常机制”的出现,是为了补充“错误机制”的不足,是为了防范可能出现的错误。优雅的处理错误和异常,是提升代码友好度和开发效率的关键。

什么是错误?

PHP中将代码自身的异常(一般是环境或者语法非法所致)称为错误(Error)

什么是异常?

PHP中将代码运行中出现的逻辑错误称为异常(Exception)。

| 异常

异常是 Exception类的对象,我们可以通过getCode(),getMessage(),getFile(),getLine() 获取详细的异常信息用于相关处理。

<?php
$e = new Exception('系统异常', 500);
$msg = sprintf("%s %s:%s", $e->getMessage(), $e->getFile(), $e->getLine());
echo $msg;  // 系统异常 /var/www/html/index.php:2

| 异常抛出

异常通常是我们主动抛出的。我们在主观上感觉某段逻辑可能出现错误的时候,我们可以抛出异常,避免程序出现错误。异常一旦抛出后程序就会立即停止执行,PHP将会尝试匹配“catch”代码块去捕获异常。如果异常未被捕获,PHP将会产生一个 Fatal error 级别的错误,并且输出未能捕获异常(Uncaught Exception ...)的提示信息。

throw new Exception('系统异常', 500);

| 异常捕获

我们应当捕获并优雅的处理异常,把可能抛出异常的代码放到 try/catch 块中,如果异常未被“catch”捕获处理的话会被向上抛出,此时PHP会检查上下文是否注册了 set_exception_handler 函数。

try {
    if (!isset($username)) {
        throw new Exception('用户不存在', 400);
    }
} catch (Exception $e) {
    echo $e->getMessage();
}

| 异常处理

set_exception_handler 函数是 用户自定义异常捕获器。如果未注册,则进入 PHP 标准错误处理,触发致命错误终止程序运行;如果已注册,则进入 set_exception_handler 处理,最终程序依然会终止执行。

函数方式:

// 普通函数方式
set_exception_handler('exception_handler');

function exception_handler(Exception $e) {
    echo $msg = sprintf("%s %s:%s", $e->getMessage(), $e->getFile(), $e->getLine());
}

//或者匿名函数方式
set_exception_handler(function (Exception $e) {
    echo $msg = sprintf("%s %s:%s", $e->getMessage(), $e->getFile(), $e->getLine());
});

throw new Exception('用户不存在', 400);

类方式:

//设置顶层异常处理器
set_exception_handler([new Handler(), 'exceptionHandler']);

class Handler
{
    public function exceptionHandler(Exception $exception)
    {
        //错误信息+文件+行号
        $msg = sprintf("%s %s:%s", $exception->getMessage(), $exception->getFile(), $exception->getLine());
    }
}

throw new Exception('用户不存在', 400);

| 错误

PHP 的错误处理其实可以分为:用户自定义错误处理 和 PHP标准错误处理,两者的关系相当于两层错误捕捉器,有独立捕获级别。系统会先检测是否注册了 用户自定义错误处理,否则会将错误交由 PHP标准错误处理进行处理。

| 错误类型

PHP中的错误类型一般是解析错误、运行时错误(致命错误、非致命错误)。

解析错误:Parse error,一般伴随着 syntax error 的错误原因,说明程序不符合PHP的语法,它是级别最高的错误。在通过系统语法检测的过程中,如果遇到语法解析错误,整个程序将不会执行直接抛出错误并且这个错误不能被捕获常见于语句末尾没有加 

PHP Parse error:  syntax error, unexpected end of file in ...

运行时错误:这种错误一般不会阻止PHP脚本的执行,但会阻止当前要做的事情,抛出一条错误。常见的就是WarningNotice错误,Fatal error 也属于运行时错误,但是会终止程序运行。

// 例如通知错误
PHP Notice:  Undefined variable: params in ...

致命错误:Fatal error级别仅次于 Parse error 错误,程序将终止执行。常见报错原因:调用未定义函数、require不存在的文件、调用函数参数缺失等。

// 例如调用未定义函数
PHP Fatal error:  Uncaught Error: Call to undefined function fun() in ...

| 错误级别

级别常量                        错误值            错误类型                描述

# 系统级用户代码错误类型    try/catch 可捕获PHP7中 set_exception_handler 可捕获

E_PARSE                      4                     解析时错误            致命错误

E_ERROR                     1                     运行时错误            致命错误

致命错误,set_error_handler 可捕获

E_WARNING                 2                     运行时警告            --

E_NOTICE                    8                      运行时提醒            --

E_DEPRECATED         8192                运行时已废弃         --

# 用户级自定义错误    可由 trigger_error 触发    可由 set_error_handler 捕获处理

E_USER_ERROR         256                 用户自定义错误     致命错误,未捕获处理将终止程序。

E_USER_WARNING     512                 用户自定义警告     非致命错误

E_USER_NOTICE        1024                用户自定义通知     --

E_USER_DEPRECATED       16384    用户自定义运行时已废弃的函数或方法

# Zend Engine 相关的一些错误 内存错误一类的

E_CORE_ERROR

E_CORE_WARNING

E_COMPILE_ERROR

E_COMPILE_WARNING

# 编码标准化警告(建议如何修改以向前兼容)

E_STRICT                                 2048    编码标准化警告    部分 try/catch,部分 set_error_handler

E_RECOVERABLE_ERROR    4096    编码标准化警告    set_error_handler 可捕获

| PHP标准错误处理

PHP 标准错误处理是在一些错误没有被用户捕获处理(try/catch 或 set_error_handler)时,错误会递交至 PHP标准错误处理。

1、display_errors:是否开启PHP输出错误报告的级别。

 值为:On (默认的输出错误报告)、Off (屏蔽所有的错误信息)

 -- 在PHP脚本中可以调用ini_set( ) 函数,动态设置php.ini配置文件。

 -- 如:ini_set("display_errors", true); 显示所有的错误信息

2、error_reporting: 设置不同的错误级别报告

error_reporting = E_ALL & ~E_NOTICE

-- 可以抛出任何非提醒的错误

error_reporting = E_ERROR | E_PARSE | E_CORE_ERROR

-- 只考虑致命的运行时错误,解析错误和核心错误。

error_reporting = E_ALL & ~(E_USER_ERROR | E_USER_WARNING | E_USER_NOTICE)

-- 报告用户导致的错误之外的所有错误。

在PHP脚本可以通过error_reporting() 函数动态设置错误报告级别。

如:error_reporting(E_ALL);

注意:开发环境显示全部错误;生产环境隐藏全部错误;任何环境都要记录日志。

<?php
// 开启错误信息显示,将错误输出至标准输出(浏览器/命令行)
ini_set('display_errors', true);
// 监听捕获的错误级别
error_reporting(E_ALL);
// 开启错误日志记录
ini_set('log_errors', true);
// 指定错误日志文件
ini_set('error_log', __DIR__ . '/errors.log');

| 用户自定义错误处理

set_error_handler 函数是 用户自定义错误捕获器。如果注册了 set_error_handler ,错误将会进入自定义处理,且不受 error_reporting 设定的级别的影响且不会终止程序继续执行,但是 set_error_handler 并非可以捕获所有错误,默认处理 E_ALL | E_STRICT 级别错误。处理后若返回 false/exit()/die(),则错误会被继续递交给 PHP标准错误处理 流程。

可以捕获: E_WARNING & E_NOTICE & E_DEPRCATED & E_USER_* & 部分 E_STRICT 级的错误。

无法捕获: E_ERROR & E_PARSE & E_CORE_* & E_COMPLIE_* & 部分 E_STRICT 级的错误。

<?php
//这里用类调用方式
//设置顶层错误处理器
set_error_handler([new Handler(), 'errorHandler']);

class Handler
{
    public function errorHandler($errNo = 0, $errMsg = '', $errFile = '', $errLine = 0)
    {
        $msg = sprintf("%s %s:%s", $errMsg, $errFile, $errLine);
        echo $msg;
    }
}

| trigger_error

trigger_error 用来触发 用户级别的自定义错误,可以在 set_error_handler 中捕获处理。默认的错误级别是 E_USER_NOTICE。

trigger_error('用户自定义 notice');
trigger_error('用户自定义 fatal error', E_USER_ERROR);

| try/catch

set_error_handler 是捕获不到 E_ERROR 和 E_PARSE 的,我们可以通过 try/catch 来捕获 E_ERROR 和 E_PARSE 级别的错误,并且 try/catch 错误捕获级别同样不受 error_reporting 影响

在PHP7 中,E_ERROR 和 E_PARSE 级别错误会被作为 Error 异常 抛出,这种 Error 异常和 Exception 异常一样会被 try/catch 捕获。如果没有匹配到 catch 块,则调用 set_exception_handler 异常处理函数 进行处理。 如果未注册异常处理函数,则错误会被继续递交给 PHP标准错误处理 流程。

不过,Error 类并非继承 Exception 类,所以 catch 捕获的就不是 catch(Exception $e) {} 了,而是 catch(Error $e) {};set_exception_handler 的匿名函数同样用 Error 去捕获。但是,PHP7 中 Error 类和 Exception 类都实现了 Throwable 接口,所以我们可以使用 Throwable 来同时捕获 Error 异常 和 Exception 异常了。

<?php
set_exception_handler('exceptionHandler');

function exceptionHandler(Throwable $exception)
{
    $msg = sprintf("%s %s:%s", $exception->getMessage(), $exception->getFile(), $exception->getLine());
    echo $msg;
}

try{
    fun();
} catch (Throwable $e) {
    echo $e->getMessage();
}

| 错误委托异常处理

PHP7 中,E_ERROR 和 E_PARSE 都作为 Error 异常 抛出。set_error_handler 捕获到错误后,程序仍会继续执行,所以我们可以把捕获到的错误委托给异常进行统一处理。

<?php
//设置顶层错误处理器
set_error_handler([new Handler(), 'errorHandler']);
//设置顶层异常处理器
set_exception_handler([new Handler(), 'exceptionHandler']);

class Handler
{
    public function exceptionHandler(\Throwable $exception)
    {
        $msg = sprintf("%s %s:%s", $exception->getMessage(), $exception->getFile(), $exception->getLine());
        echo $msg;
    }

    public function errorHandler($errNo = 0, $errMsg = '', $errFile = '', $errLine = 0)
    { 
        // 委托异常处理
        throw new ErrorException($errMsg, 0, $errNo, $errFile, $errLine);
    }
}

fun();  // 调用未定义的函数

//结果 Call to undefined function fun() /var/www/html/index.php:xxx

| 全局错误和异常捕获处理(PHP7)

现在我们基本能捕获所有代码层次的错误和异常,那我们就来整理下思路:

1、set_exception_handler 全局捕获 E_ERROR 和 E_PARSE 级别的 Error 和 Exception 异常。

2、set_error_handler 全局捕获 E_WARNING & E_NOTICE & E_DEPRCATED & E_USER_* & 部分 E_STRICT 级的错误,并将错误信息通过 ErrorException 抛出,交由 try/catch 或者 set_exception_handler 捕获。

3、set_exception_handler 最终将会捕获所有错误和异常,统一处理。

ps:个人比较倾向 try/catch 作为局部异常捕获,set_exception_handler 作为全局异常捕获,也可以用 try/catch 结合 set_exception_handler 来作为全局异常捕获,这样更保险,PHP7中貌似 try/catch 能捕获到的 set_exception_handler 全都能捕获到(部分错误没测试到)

<?php
// index.php 文件

//设置顶层错误处理器
set_error_handler([new Handler(), 'errorHandler']);
//设置顶层异常处理器
set_exception_handler([new Handler(), 'exceptionHandler']);

class Handler
{
    public function exceptionHandler(Throwable $exception)
    {
        /**
         * 所有的错误和异常都到这里了
         * 我们可以记错误日志,或者其他需要的操作
         */
        $msg = sprintf("%s %s:%s", $exception->getMessage(), $exception->getFile(), $exception->getLine());
        echo $msg;
    }

    public function errorHandler($errNo = 0, $errMsg = '', $errFile = '', $errLine = 0)
    {
        // 委托异常处理
        throw new ErrorException($errMsg, 500, $errNo, $errFile, $errLine);
    }
}

/**
 * 加载外部文件的正确写法
 * 当前文件不能有语法解析错误,否则PHP在语法解析时就报错,脚本压根就没执行,所有处理都没用
 * 在错误或异常处理器回调函数中,不能出现各自捕获不到的错误
 * 注册完错误和异常处理器后,可以有语法之外的错误
 * 我们引入的 base.php 文件里面可以有语法错误
 * 也就意味着,入口文件一定不能有语法解析错误,否则捕获不到,这个要从PHP的机制来说起了
 */
$file = __DIR__ . '/base.php';
if ( !file_exists($file) ) {
    throw new Exception($file . ' not exists!');
}

require_once $file;

|  版权声明:本文为博主原创文章,转载请注明出处。