一、PHP异常处理机制

        由于我的工作岗位性质,我绝大部分的开发工作涉及到的操作风险都非常高,而且很频繁地使用其他部门提供的接口。所以,对于程序中可能出现的异常和错误都要有相应的处理方法,否则遗漏的话会造成很重大的事故。而且,由于涉及到其他接口的调用,以及高危操作,所以需要对每一步的操作都做日子记录,方便后续出问题后排查定位。

        在本篇文章里,我将系统梳理下PHP的错误处理和异常处理方法和机制,让大家意识到在实际项目开发中,对PHP错误和异常处理的重要性。最后介绍Whoops和Monolog组件在实践中的使用。

1.PHP异常是什么

        PHP5引入了异常的特性,这是一种新的面向对象的错误处理方法,使用过Java做开发的人应该对异常的概念非常熟悉。

        使用try…catch块捕获PHP中出现的异常过程跟其他语言捕获异常处理几乎一样。当抛出异常时,PHP在运行时会去查找能够处理这个异常的catch语句,如果找不到则将异常传递给全局异常处理函数。抛出异常后代码会立即停止执行,后续的PHP代码都不会运行。

        异常是从PHP5内置的Exception类(或子类)实例化得到的特殊对象,Exception类对象用于存放和报告错误信息。PHP内置的异常类及其子类如下:

        除此之外,PHP标准库还提供了以下额外的Exception子类,用于扩展PHP内置的异常类:

       由于PHP中的异常是类,因此我们可以轻易扩展Exception类,使用定制的属性和方法创建自定义的异常类。使用哪个子类由开发者决定,不过选择和创建的异常子类最好能够回答为什么会抛出异常这个问题。

        下面是一个简单的异常捕获的例子:

<?php

try {
    //运行自己的代码
}
catch (exception $e) {
    //异常处理代码块
}

2.PHP异常处理关键字

        下面这几个关键字时异常处理过程中会经常用到的:

  • Try:try块里面包含了可能会抛出异常的代码,所有try块里面的代码都会执行知道潜在的异常被抛出来。
  • Throw:throw关键字表示PHP将要发生异常。PHP运行时会去查找catch语句来处理这个异常。
  • Catch:这个catch代码块只有Try代码块中发生异常的时候才会被调用,这个时候你就需要在catch代码块中处理好异常。
  • Finally:在PHP5.5中引入了finally的概念。Finally代码块可以放在catch代码块后面或者取代catch代码块。我们知道try…catch语句中异常处理后就会终止脚本执行,而如果过使用try…catch…finally语句,则无论时正常执行流程还是异常处理流程,finally块中的代码最终都会被执行。所以这个finally代码块中可以放置一些发生异常时关闭资源的情况,如关闭数据库连接等。
try {
    //运行自己的代码
}
catch (exception $e) {
    //异常处理代码块
}
finally {
    //最终处理代码
}


try {
    //运行自己的代码
}
finally {
    //最终处理代码
}
下图时try...catch...finally的执行顺序:

3.如何捕获和处理异常

        对于代码中抛出的异常,我们应该进行相应的处理,否则未捕获的异常会导致PHP应用终止运行,同时显示错误信息,更严重的是,暴露的敏感调试信息可能会给程序带来安全隐患,导致被入侵或破坏。尤其在使用第三方的PHP组件或框架时要做好异常处理。

        下面的代码是没有进行异常捕获处理,导致代码奔溃出现异常。

function foo($a, $b) {
if ($b == 0) {
throw new Exception('Division by zero.');
}
return $a / $b;
}

foo(10, 0);

输出:

Fatal error: Uncaught exception ‘Exception’ with message ‘Division by zero.’ in C:\Users\xxxxxxx\error_and_exception\demo4.php:5
Stack trace:
#0 C:\Users\xxxxx\error_and_exception\demo4.php(10): foo(10, 0)
#1 {main}
thrown in C:\Users\xxxxxxx\error_and_exception\demo4.php on line 5

        那么,下面我们看看,异常捕获和处理有哪些基本方法呢

(1)Try, throw 和catch

       对上面示例中的代码,我们通过try…catch捕获异常。这是程序就不会输出错误的堆栈信息。

<?php

function foo($a, $b) {
    if ($b == 0) {
        throw new Exception('Division by zero.');
    }
    return $a / $b;
}

try {
    foo(10, 0);
} catch (Exception $e) {
    echo "Caught exception:" . $e->getMessage();
}

输出:

Caught exception:Division by zero.

(2)自定义的Exception类

        由于Exception也是类,我们可以通过继承Exception创建自己的异常子类。

<?php

class DivisionException extends Exception {
    public function __toString()
    {
        return "Division by zero.";
    }
}

function foo($a, $b) {
    if ($b === 0) {
        throw new DivisionException('Division by zero.');
    }
    return $a / $b;
}

try {
    foo(10, 0);
} catch (Exception $e) {
    echo "Caught exception:" . $e->getMessage();
}

输出:

Caught exception:Division by zero.

(3)多个异常

        如何try代码块中可能有多种类型的异常需要处理时,我们可以使用多个catch块进行拦截。同时,我们还可以使用finally语句块,在捕获任何异常以后运行一段代码(未捕获到异常也会运行)。

<?php

class DivisionException extends Exception {
    public function __toString()
    {
        return "Division by zero.";
    }
}

function foo($a, $b) {
    if ($b === 0) {
        throw new DivisionException('Division by zero.');
    }
    return $a / $b;
}

try {
    foo(10, 0);
} catch (DivisionException $e) {
    echo "Caught division exception: " . $e->getMessage();
} catch (Exception $e) {
    echo "Caught other exception:" . $e->getMessage();
} finally {
echo 'Always do this';
}
输出:
Caught division exception: Division by zero.Always do this

        第一个catch块会捕获DivisionException异常,第二个catch块会捕获所有类型的异常。捕获某种异常时,指挥运行其中一个catch块,如果PHP没有找到合适的catch块,异常会向上冒泡(调用该代码块所在函数的上一级函数),直到PHP脚本由于Fatal Error错误而终止运行。

(4)全局异常处理函数

        如果我们无法确认所有可能抛出的异常,然后又想要尽可能去捕获每一个可能抛出的异常,那这个要怎么实现呢?

        我们可以通过注册一个全局异常处理函数,来捕获所有未被捕获的异常。这个全局异常处理函数一定要设置,它是我们最后异常处理的安全保障。对于未捕获的异常,可以通过全局异常处理函数来给PHP应用的用户现实一个合适的错误信息。

        全局异常处理函数:

<?php
set_exception_handler(function (Exception $e) {
    // 处理并记录异常
});

参数可以匿名函数,也可以时回调函数。

<?php
function handleException($e)
{
        echo "Handle by global exception handler:" . $e->getMessage();
}
set_exception_handler("handleException");

class DivisionException extends Exception {
    public function __toString()
    {
        return "Division by zero.";
    }
}

class UnknownException extends Exception {
    public function __toString()
    {
        return "Unknown exception.";
    }
}

function foo($a, $b) {
    if ($b === 0) {
        throw new DivisionException('Division by zero.');
    }
    if ($a < 0) {
        throw new UnknownException('Invalid negative number.');
    }
    return $a / $b;
}

try {
    foo(-10, 2);
} catch (DivisionException $e) {
    echo "Caught division exception: " . $e->getMessage();
}

输出:

Handle by global exception handler:Invalid negative number.

上面的代码,我们就通过set_exception_handler函数捕获了一个未被捕获到的异常,应用该函数就可以捕获所有未被捕获的异常。

4.小结

异常处理规则:

  • 可能会抛出异常的代码应该放在try代码块内,以便捕获潜在的异常
  • 每个try或throw代码块必须至少有一个对应的catch代码块
  • 使用多个catch代码块可以捕获不同种类的异常
  • 可以在try代码块内的catch代码块中再次抛出异常

一句话:如果出现了异常,就必须捕获并处理

二、PHP错误处理机制

1.PHP错误分类

        PHP错误和异常还是有点差别,PHP脚本由于某些原因而导致无法运行,通常或出发错误。当然,我们也可以通过trigger_error函数自己触发错误,然后使用自定义的错误处理函数进行处理。

        现在一共有16个错误级别(参考官网),包括E_ERROR、E_WARNING、E_NOTICE和E_PARSE等。

        其错误级别如下:

Parse error > Fatal Error > Waning > Notice 

  • Parse Error语法解析错误:

语法检查阶段报错并中断执行,需要修改代码。

例如未标分号:

<?php
$a= 3

输出:

Parse error: syntax error, unexpected end of file in C:\Users\xxxxx\error_and_exception\demo5.php on line 2
  • Fatal Error错误级别的错误:

程序直接报错并中断执行,需要修改代码,无法通过set_error_handler捕获,可以使用register_shutdown_function在程序终止前出发一个函数。

例如使用未定义的函数:

<?php
noexist();  //Fatal error: Call to undefined function noexist()

输出:

Fatal error: Call to undefined function noexist() in C:\Users\xxxx\error_and_exception\demo5.php on line 2
  • Warning警告级别的错误:

           程序出问题,但程序继续执行,需要修改代码。

例如参数个数不一直:

<?php
// Warning
$a = ['o' => 2, 4, 6, 8];
// 由于array_sum只能接收一个参数,所以这里会报Warning
$result = array_sum($a, 3);

输出:

Warning: array_sum() expects exactly 1 parameter, 2 given in C:\Users\xxxx\error_and_exception\demo5.php on line 5
  • Notice通知级别的错误:

如使用一些未定义的变量、或者数组key没有加引号的时候会出现,但程 序继续执行

例如使用未定义的变量:

<?php
// E_NOTICE 运行时通知 表示脚本遇到可能会表现为错误的情况  脚本会继续执行
$a = $b; //Notice: Undefined variable

输出:

Notice: Undefined variable: b in C:\Users\xxx\error_and_exception\demo5.php on line 3

2.PHP错误配置

(1)在脚本中设置

  • ini_set(‘display_errors’, 0); //关闭错误输出

在脚本中可以通过设置该配置,关闭或开启错误输出。我们的原则是:开发环境开启,生产环境关闭

  • error_reporting(E_ALL & ~E_NOTICE); //报告 E_NOTICE 之外的所有错误

可以通过设置error_reporting设置想要报告的错误级别。

(2)在php.ini配置文件中配置

上面两个的配置同样可以在php.ini中配置,

  • error_reporting = E_ALL&~E_NOTICE; //报告E_NOTICE之外的所有错误
  • display_errors = 1; //输出错误

3.PHP错误处理

(1)set_error_handler

        这个错误处理函数只能处理Deprecated、Notice和Warning这三种级别的错误,也就是说无法处理Fatal Error和Parse Error这两种错误。而且这个函数在处理后,脚本会继续执行发生错误的下一行代码。

备注:这里需要注意的是try…catch是无法捕获错误的。

        那么,我们能不能对PHP的错误处理也采用异常处理机制来处理呢?答案是可以的,我们可以在设置的错误处理函数抛出异常,这样就可以利用异常处理机制来捕获和处理了。如下:

<?php
ini_set('display_errors', 1); //开启错误输出
error_reporting(E_ALL); //报告所有错误级别


function handleError($errno, $errstr, $erfile, $errline) {
    throw new Exception($errstr);
}

set_error_handler("handleError");

try {
    $a = $b; //Notice: Undefined variable
} catch (Exception $e) {
    echo "Caught exceptioin: " . $e->getMessage() . PHP_EOL;
}


try {
    // Warning
    $a = ['o' => 2, 4, 6, 8];
    // 由于array_sum只能接收一个参数,所以这里会报Warning
    $result = array_sum($a, 3);
} catch (Exception $e) {
    echo "Caught exceptioin: " . $e->getMessage() . PHP_EOL;
}

try {
    //set_error_handler无法处理Fatal error错误
    noexist();  //Fatal error: Call to undefined function noexist()
} catch (Exception $e) {
    echo "Caught exceptioin: " . $e->getMessage() . PHP_EOL;
}

输出:

Caught exceptioin: Undefined variable: b
Caught exceptioin: array_sum() expects exactly 1 parameter, 2 given

Fatal error: Call to undefined function noexist() in C:\Users\xxx\error_and_exception\demo5.php on line 30

同样的Parse error错误set_error_handler也是无法处理的:

try {
    //set_error_handler无法处理Parse error错误
    $a=3
} catch (Exception $e) {
    echo "Caught exceptioin: " . $e->getMessage() . PHP_EOL;
}

输出:

Parse error: syntax error, unexpected ‘}’ in C:\Users\xxxx\error_and_exception\demo5.php on line 38

        那么对于Fatal error错误(Parse error在编译时就报错),我们有没有办法用其他方法处理呢?请看下面的函数。

(2)register_shutdown_function

        这个方法是在脚本结束前的最后一个回调函数,也就是说无论是错误还是异常还是脚本正常结束都会调用。我们就可以通过注册这个shutdown函数结合error_get_last函数,来对Fatal error错误发生时进行相应的处理,如通知或释放资源之类的操作。

<?php
ini_set('display_errors', 1); //开启错误输出
error_reporting(E_ALL); //报告所有错误级别

function shutdown() {
    $errstr = error_get_last();
    var_dump($errstr);
    echo "Last function to execute!";

}

register_shutdown_function('shutdown');

//set_error_handler无法处理Fatal error错误
noexist();  //Fatal error: Call to undefined function noexist()

输出:

Fatal error: Call to undefined function noexist() in C:\Users\guanc\Desktop\error_and_exception\demo6.php on line 15
array(4) {
[“type”]=>
int(1)
[“message”]=>
string(36) “Call to undefined function noexist()”
[“file”]=>
string(52) “C:\Users\guanc\Desktop\error_and_exception\demo6.php”
[“line”]=>
int(15)
}
Last function to execute!

4.小结

        最后,总结一下错误处理这一部分的内容。

        在开发环境中,我们一般是显示并记录所有错误信息,而在生产环境中我们会让PHP记录大部分错误信息,但是不会显示出来。总的原则是:

  • 一定要让 PHP 报告错误
  • 在开发环境中要显示错误
  • 在生产环境中不能显示错误
  • 在开发环境和生成环境中都要记录错误

三、PHP错误、异常处理实践

1.Whoops组件

        在实际开发中,PHP默认的错误显示对阅读时不友好的,排查定位问题也不妨斌。然后通过Whoops组件可以为PHP错误和异常提供精美和易于阅读的诊断页面。

【Packagist】:https://packagist.org/packages/filp/whoops

【GitHub】:https://github.com/filp/whoops

【安装】:通过composer进行安装

composer require filp/whoops

【使用】:Whoops使用很简单,只需要把下列代码放到项目的引导文件中就可以。如果想要捕获所有的错误和异常,就必须在程序的开头注册组件。

$whoops = new \Whoops\Run;
$whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler);
$whoops->register();

示例:

<?php

require __DIR__ . '/vendor/autoload.php';

$whoops = new \Whoops\Run;
$whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler);
$whoops->register();

$a = $b;

然后通过浏览器访问这个php脚本,可以看到下面这个很漂亮的诊断页面:

2.Monolog组件

        monolog是日志记录组件,遵循PSR-3规范。我们在处理PHP错误和异常时,无论是生产环境还是开发环境,都需要把错误和异常记录下来。Monolog作为日志组件能很出色完成这个任务。

【Packagist】:https://packagist.org/packages/filp/whoops

【GitHub】:https://github.com/Seldaek/monolog

【安装】:通过composer进行安装

composer require monolog/monolog

【使用】:

<?php
require __DIR__ . '/vendor/autoload.php';
use Monolog\Logger;
use Monolog\Handler\StreamHandler;


try {
    // create a log channel
    $log = new Logger('name');
    $logName = date('Y-m-d',time()) . '.log';
    $log->pushHandler(new StreamHandler($logName, Logger::DEBUG));
    // add records to the log
    $log->info('This is a test');
    $log->warning('Foo');
    $log->error('Bar');
} catch (Exception $e) {
    var_dump($e->getMessage());
}

我们看下记录的日志:

λ cat 2018-12-16.log
[2018-12-16 12:26:52] name.INFO: This is a test [] []
[2018-12-16 12:26:52] name.WARNING: Foo [] []
[2018-12-16 12:26:52] name.ERROR: Bar [] []

 

最后推荐一个不错的视频教程,教程的作者是BAT的工程师以及《深入PHP内核》书籍的作者,想要提升PHP水平的朋友推荐可以学习一下:

课程地址:《PHP 进阶之路(工作三年技术还是停滞不前?上车!)》

打赏

1 对 “深入学习PHP错误与异常处理”的想法;

发表评论

电子邮件地址不会被公开。