如何处理 PHP 的错误与异常(笔记)

405 查看

这话题已经没有什么新意了,这里只是做做笔记,作为思路的一种整理,也以便后续忘了可以回来这里查找。

错误

以下是 PHP 最常见的几种错误:

// E_NOTICE
echo $a;

// E_WARNING
echo 100 / 0;

class Sample
{
    public function method()
    {
        //not static method
    }
}

// E_STRICT
Sample::method();

// E_ERROR
new Dummy();

运行上面代码,页面输出以下信息:

Notice: Undefined variable: a in D:\errors-exceptions\demo4.php on line 6

Warning: Division by zero in D:\errors-exceptions\demo4.php on line 9

Strict Standards: Non-static method Sample::method() should not be called statically in D:\errors-exceptions\demo4.php on line 20

Fatal error: Class 'Dummy' not found in D:\errors-exceptions\demo4.php on line 23

在生产环境下,是不允许把错误信息输出到页面的。

怎么办?关闭错误输出

ini_set('display_errors', 0);

此时,刷新页面,页面将不会报任何错误。页面一片空白,或者显示 500 错误。

这也不是我们希望的,虽然不把错误输出到页面,但是这些错误我们是希望把它们都收集起来,写到日志里面,以便开发人员能够不断改进代码,排查错误。

怎么办?自定义错误处理

set_error_handler(function($errno, $errstr, $errfile, $errline) {

    //在这里对错误进行处理

    echo $errstr . '<br>';
});

运行上面的代码,页面输出:

Undefined variable: a
Division by zero
Non-static method Sample::method() should not be called statically

很奇怪,不是应该输出 4 个错误吗?怎么 Fatal error 没有捕捉到。查看 set_error_handler 帮助文档,我们发现,它有以下描述:

以下级别的错误不能由用户定义的函数来处理: E_ERROR、 E_PARSE、 E_CORE_ERROR、 E_CORE_WARNING、 E_COMPILE_ERROR、 E_COMPILE_WARNING,和在 调用 set_error_handler() 函数所在文件中产生的大多数 E_STRICT。

噢~,原来是这样,原来 set_error_handler 方法是不能捕捉 E_ERROR 错误的(上面的 Fatal error)。那么这类错误,我们是不能放过的,也必须捕捉到,并做适当的处理。

怎么办? 利用 register_shutdown_function 函数

register_shutdown_function(function() {

    if(is_null($e = error_get_last()) === false) {

        echo $e['message'] . '<br>';
    }
});

此时,再运行上面的例子,页面会输出:

Undefined variable: a
Division by zero
Non-static method Sample::method() should not be called statically
Class 'Dummy' not found

很好,四种错误,我们都成功捕捉了,并按照我们自己的方式进行了输出。

到目前为止,应该说能捕捉的 PHP 错误,我们都捕捉到了,当然还有一些错误,压根就没有办法捕捉。比如语法错误,这类错误是在 PHP 引擎对即将执行的文件编译期间的错误。

<?php

sfdsdfdsfsfsdf

//页面输出
Parse error: syntax error, unexpected end of file

像上面这种错误,程序就没有办法捕捉了。只要程序是经过测试的,一般不会在生产环境出现此类错误,所以也不用过于但心。

最后注:register_shutdown_function 函数会在任何导致页面退出的时候,会被调用。比如发生了致命错误、使用 exit() 函数、又或者是页面执行完毕了,都会触发该函数。利用这个特征,我们可以在页面退出时,获取到最后一个错误,然后进行记录。这个的 “最后一个错误” 往往是致命错误,原因很简单:因为一旦错误被 set_error_handler 捕捉到了,那么 register_shutdown_function 将捕捉不了。而 前者捕捉不了的,才会被后者捕捉。

异常

在 PHP 中,所以异常的基类都是 Exception。异常应该说是在 PHP 后来引入了面向对象的概念后,才有的产物。那么说,PHP 原来只抛错误,却没有异常的概念了。但现在,异常的使用已经非常广泛了,我们有必要学习一下。

好,我们来制造一些异常:

new PDO('mysql:dbname=testdb;host=127.0.0.1', 'root', 'wrong_passwd');

//运行结果
Fatal error: Uncaught exception 'PDOException' with message 'SQLSTATE[HY000] [1045] Access denied for user 'root'@'localhost' (using password: YES)' in D:\errors-exceptions\demo6.php:6 Stack trace: #0 D:\errors-exceptions\demo6.php(6): PDO->__construct('mysql:dbname=te...', 'root', 'wrong_passwd') #1 {main} thrown in D:\errors-exceptions\demo6.php on line 6

再制造一个:

throw new Exception('我是异常');

//运行结果
Fatal error: Uncaught exception 'Exception' with message '我是异常' in D:\errors-exceptions\demo6.php:9 Stack trace: #0 {main} thrown in D:\errors-exceptions\demo6.php on line 9

在生产环境下,页面直接输出这些异常,同样是不优雅的,那么我们同样可以像关闭错误输出一样,关闭异常的输出:

ini_set('display_errors', 0);

同样地,异常虽然不显示出来了,但是我们需要记录并处理这些异常。

怎样做? 使用 set_exception_handler 函数

set_exception_handler(function($exception) {

    //在这里,统一处理异常
    
    echo get_class($exception) .': '. $exception->getMessage();
});

此时,再运行页面,会输出以下信息:

PDOException: SQLSTATE[HY000] [1045] Access denied for user 'root'@'localhost' (using password: YES)

小结

在我刚开始学习 PHP 的时候,的确被它的错误和异常困扰了许久。最开始,我甚至不会用异常。实际上,在现代的 PHP 里面,我们基本上可以完全控制它的异常和错误。只需要分开处理就可以了。

扩展:我们在实际项目中捕捉并处理异常和错误的时候,往往是把两者合二为一。就是说把异常也当成错误来处理。又或者反过来,把错误当成异常来处理。

下面,就让我们一起看看,如何把两者相互转换:

把异常转换为错误处理

<?php 

register_shutdown_function('shutdownHandler');
set_error_handler('errorHandler');
set_exception_handler('exceptionHandler');

//自定义错误处理
function errorHandler($code, $message, $file, $line) {

    //在这里,对错误进行后续处理。比如:写日志、发邮件等等

    echo ' Code: '.$code
        .', Message: '.$message
        .', File: ' . $file
        .', Line: ' . $line
        . '<br>';
}

//自定义异常处理
function exceptionHandler($exception) {
    errorHandler(
        $exception->getCode(),
        $exception->getMessage(),
        $exception->getFile(),
        $exception->getLine()
    );
}

//自定义致命错误处理
function shutdownHandler() {
    if(is_null($e = error_get_last()) === false) {
        errorHandler(
            $e['type'],
            $e['message'],
            $e['file'],
            $e['line']
        );
    }
}

不过这样处理法,会导致异常的堆栈信息“丢失”,意思是无法处理这些堆栈信息了。当然是有办法处理的了,具体请看 demo3.php

再看看,如何把错误转换为异常 (注意:以下代码仅为了演示其原理,代码本身并非完全合理)

set_error_handler('errorHandler');

function errorHandler($errno, $errstr, $errfile, $errline) {
    throw new ErrorException($errstr, $errno, 0, $errfile, $errline);
};

大家看到,我们使用了 ErrorException 这个类,这是 PHP 后来才引入的,叫作错误异常。当然,我猜它的目的,应该也是为了能实现错误与异常之间优雅转换而添加的。

至此,错误与异常的学习基本完毕。最后推荐看看一个网站有关于对 PHP 错误与异常的介绍,尤其是对异常的一些行为的说明,都是值得注意的,网站在这里:PHP - Error & Exception Handling

参考文献

  1. PHP在什么时候应该使用异常处理(Exception)?

  2. PHP Trick: Catching fatal errors (E_ERROR) with a custom error handler