接触yii2之前,很多同学应该都接触过其他php框架,比如一些cms。
早期的cms框架,当然也不排除现在依然存在的一个问题:页面上经常会抛出类似下面的警告或者错误1
PHP message: PHP Warning: Division by zero in /private/var/www/test/index.php on line 3
这是一个非常严重的安全问题,当然你可以去修改配置屏蔽掉类似的一系列错误。这并不是我们要说的重点,此处就不再继续深究下去了。
你应该注意到了一个现象,不管你在 php.ini 文件内怎么配置,在yii2的应用中,类似上面的现象都不会发生。有同学可能要说这个操作并不难,程序中动态修改配置就好了呀。
但是更重要的是yii2会为你呈现更详细的调用栈和错误信息,这对我们来说是非常友好的。
也就是说在yii2中,对错误的处理方式并不等同于原生的php标准错误了,这是怎么做到的?try/catch 捕获异常,然后再向用户展示错误信息?
我们知道,try/catch 捕获的异常是有限的。所以在php中,有了 set_exception_handler 函数专门处理未被 try/catch 捕获的异常。
这个函数是用户自定义异常处理的函数。
有了 try/catch 和 set_exception_handler 是不是就能保证程序中的异常都能完全由应用程序处理了呢?
感觉是这么回事。我们看一个基本的一个例子1
2
3
4
5
6
7
8
9
10
11
12function myException($exception)
{
echo "MyException: " , $exception->getMessage();
}
set_exception_handler('myException');
try {
1/0;
} catch (Exception $exception) {
echo "Exception:" , $exception->getMessage();
}
直接运行,我们看下结果1
Warning: Division by zero in /private/var/www/test/index.php on line 11
很明显这是一个warning级别的错误并且我们没有捕获到这个warning。
其实在php中,类似程序中的warning、notice信息,需要另外一个函数捕获 set_error_handler,简单的修改下上面的代码,我们看下结果1
2
3
4
5
6
7
8
9
10
11function myError($errno, $errstr, $errfile, $errline)
{
echo "[$errno] $errstr \n";
}
set_error_handler('myError');
try {
1/0;
} catch (Exception $e) {
}
结果1
[2] Division by zero
尽管如此,set_error_handler 函数还是不够完善,手册上是这么描述的:以下级别的错误不能由用户定义的函数来处理: E_ERROR、 E_PARSE、 E_CORE_ERROR、 E_CORE_WARNING、 E_COMPILE_ERROR、 E_COMPILE_WARNING,和在 调用 set_error_handler() 函数所在文件中产生的大多数 E_STRICT。
也就是说,对于php初始化或者编译等产生的核心错误,我们并不能捕获并处理掉。
但是我们想这么做。有没有办法解决呢?大家可以思考一下,前面我们应该提到过一个函数。
上面是从php的角度简单的看待异常的处理。yii2提供的异常处理机制是相当完善的,这也算是yii2的一个优势吧,下面我们一起来分析一下它是怎么实现的。
前面跟大家一起分析应用初始化的时候,我们就简单的提到过异常处理程序。
大家可以找到 yii\base\Application::__construct 方法1
$this->registerErrorHandler($config);
这里即是在应用中注册 errorHandler 组件。
我们从 yii\base\Application::registerErrorHandler 方法的实现开始追起。1
2
3if (YII_ENABLE_ERROR_HANDLER) {
// code
}
YII_ENABLE_ERROR_HANDLER 常量在 yii\BaseYii 类中定义为 true,所以说yii2内默认提供 errorHandler 组件为我们处理异常。当然啦,如果你有需要,完全可以自己再写一套诸如此类的实现。
下面看下这几行代码的含义1
2
3
4
5// 把 errorHandler 组件的定义保存起来
$this->set('errorHandler', $config['components']['errorHandler']);
unset($config['components']['errorHandler']);
// 获取 errorHandler 组件,并调用 yii\web\ErrorHandler::register 方法开始执行
$this->getErrorHandler()->register();
errorHandler 组件,默认指的是 yii\web\ErrorHandler, 它继承自 yii\base\ErrorHandler 。
上面调用 errorHandler::register 方法,指的就是 yii\base\ErrorHandler::register 方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public function register()
{
// 动态修改配置,避免错误信息直接显示到页面上
ini_set('display_errors', false);
// 自定义异常处理函数
set_exception_handler([$this, 'handleException']);
// 自定义错误处理函数,HHVM我们暂时先不考虑
if (defined('HHVM_VERSION')) {
set_error_handler([$this, 'handleHhvmError']);
} else {
set_error_handler([$this, 'handleError']);
}
if ($this->memoryReserveSize > 0) {
$this->_memoryReserve = str_repeat('x', $this->memoryReserveSize);
}
// 注册程序终止前执行的函数
register_shutdown_function([$this, 'handleFatalError']);
}}
下面我们就这三种异常信息来看看yii2分别是如何实现的。1
yii\base\ErrorHandler::handleException
假设我们在 site/index 操作内添加如下代码1
throw new \Exception("This is a Exception");
访问 site/index 时,yii\base\ErrorHandler::handleException 回调函数就会被触发。
触发之后,我们看一下 yii\base\ErrorHandler::handleException 又是怎么处理的,为什么有的时候会显示错误栈信息,有的时候会把信息记录到日志中。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// ......
// 如果我们在处理异常的过程中又发生了异常怎么办?所以下面用 try/catch 应对
try {
// 记录日志
$this->logException($exception);
// 丢弃其他任何的输出
if ($this->discardExistingOutput) {
$this->clearOutput();
}
// 渲染exception
$this->renderException($exception);
// ...
} catch (\Exception $e) {
// an other exception could be thrown while displaying the exception
$this->handleFallbackExceptionMessage($e, $exception);
} catch (\Throwable $e) {
// additional check for \Throwable introduced in PHP 7
$this->handleFallbackExceptionMessage($e, $exception);
}
// ......
上面贴一下 yii\base\ErrorHandler::handleException 方法的主要逻辑。
其中,日志记录部分取决于 Yii::error 方法的实现,如果在处理异常的过程中又发生了异常,则依赖 yii\base\ErrorHandler::handleFallbackExceptionMessage 方法了,这个方法就简单多了,直接输出错误信息,不过会有 YII_DEBUG 是否开启的区别,注意对待。
下面我们主要分析一下exception的渲染过程。我们把注意力集中到 yii\web\ErrorHandler::renderException 方法。
渲染的过程并不是简单的render,这其中要分好几种可能,比如说是否是接口(json)的返回?还是render一个页面?亦或是一个ajax请求?如果是接口应该返回哪些数据?如果是render一个页面,页面的模板有没有,有的话可不可以自定义?等等问题,这些都是 yii\web\ErrorHandler::renderException 方法为我们实现的。
有一点需要说明,如果异常是 UserException 的实例,实质上 UserException 也是 \Exception 的子类。程序中往往会这样判断($exception instanceof UserException)来区分 exception 是否是用户主动抛出的异常。这一点大家可以在 yii\web\ErrorHandler::renderException 方法的下面这行代码中看出1
2$useErrorView = $response->format === Response::FORMAT_HTML && (!YII_DEBUG || $exception instanceof UserException);
yii\base\ErrorHandler::handleError
首先我们可以在 site/index 操作内,用 trigger_error 触发一个error1
trigger_error('This is a error.');
当然啦,实际的程序代码中肯定不会以这么幼稚的方式去触发,这里只是一个例子,不必要过于纠结。
yii\base\ErrorHandler::handleError 方法的实现,主要是 yii\base\ErrorException 异常的抛出,抛出的异常,还是会经由 yii\base\ErrorHandler::handleException 方法捕获,最终渲染。
最后让我们看一下 register_shutdown_function 方法注册的 yii\base\ErrorHandler::handleFatalError 方法。
如果说编译异常或者php核心错误,yii2又是怎么实现的呢?
yii\base\ErrorHandler::handleFatalError 方法首先会通过 error_get_last 方法获取错误信息,然后再判断错误类型是否是核心错误,这包括 E_ERROR, E_PARSE, E_CORE_ERROR 等类型的错误,这一点可以在 yii\base\ErrorException::isFatalError 方法中找到答案。后面的实现过程无疑同前面的一致,记录错误,渲染异常等。
以上便是我们今天要介绍的异常处理的实现,希望能给你带来丁点帮助。