Yii2使用errorHandler组件来管理异常,errorHandler默认映射为ErrorHandler类
//yii/vendor/yiisoft/yii2/web/Application.php
public function coreComponents()
{
return array_merge(parent::coreComponents(), [
'request' => ['class' => 'yii\web\Request'],
'response' => ['class' => 'yii\web\Response'],
'session' => ['class' => 'yii\web\Session'],
'user' => ['class' => 'yii\web\User'],
'errorHandler' => ['class' => 'yii\web\ErrorHandler'],
]);
}
在配置文件初始化后,会有专门的方法来注册异常管理类
//yii/vendor/yiisoft/yii2/base/Application.php
protected function registerErrorHandler(&$config)
{
if (YII_ENABLE_ERROR_HANDLER) {
if (!isset($config['components']['errorHandler']['class'])) {
echo "Error: no errorHandler component is configured.\n";
exit(1);
}
$this->set('errorHandler', $config['components']['errorHandler']);
unset($config['components']['errorHandler']);
$this->getErrorHandler()->register();
}
}
通过YII_ENABLE_ERROR_HANDLER常量来判断是否使用yii本身的异常管理,这是yii唯一一个有开关选项的默认组件,仔细想想其实是有道理的,因为如果yii强行注册了异常组件,然后用户又想封装一下自己的异常组件,会导致有多个set_error_handler和set_exception_handler,即使使用restore_error_handler和restore_exception_handler也是无法保证会把yii异常组件回收的,因为无法知道已经注册了多少个异常处理层级
<?php
set_exception_handler(function(){ //假设这是yii的异常组件
//do somethings
});
set_exception_handler(function(){ //假设这是用户自定义的异常组件
throw new Exception("error"); //这个异常又会被yii的异常组件捕获
});
YII_ENABLE_ERROR_HANDLER常量可以在入口文中更改,无法在config配置中更改,因为入口文件是先加载Yii.php,后加载config,而YII_ENABLE_ERROR_HANDLER是写在Yii.php中的
//yii/vendor/yiisoft/yii2/base/BaseYii.php
defined('YII_ENABLE_ERROR_HANDLER') or define('YII_ENABLE_ERROR_HANDLER', true);
异常组件会注册handleException、handleError、handleFatalError三个方法
public $memoryReserveSize = 262144;
public function register()
{
ini_set('display_errors', false); //不显示默认带大红框的那种报错
set_exception_handler([$this, 'handleException']); //注册exception函数
if (defined('HHVM_VERSION')) {
set_error_handler([$this, 'handleHhvmError']);
} else {
set_error_handler([$this, 'handleError']); //注册error函数
}
if ($this->memoryReserveSize > 0) {
$this->_memoryReserve = str_repeat('x', $this->memoryReserveSize);
}
register_shutdown_function([$this, 'handleFatalError']); ////注册shutdown函数
}
这里值得注意的是会默认占用256KB的内存大小作为memoryReserver,防止错误导致内存不足造成无法记录和显示错误信息
handlerError函数用来捕获非error异常,将捕获到的异常封装为ErrorException类,然后抛出去让handleException函数捕获
public function handleError($code, $message, $file, $line)
{
if (error_reporting() & $code) {
// load ErrorException manually here because autoloading them will not work
// when error occurs while autoloading a class
if (!class_exists('yii\\base\\ErrorException', false)) {
require_once __DIR__ . '/ErrorException.php';
}
$exception = new ErrorException($message, $code, $code, $file, $line);
// in case error appeared in __toString method we can't throw any exception
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
array_shift($trace);
foreach ($trace as $frame) {
if ($frame['function'] === '__toString') {
$this->handleException($exception);
if (defined('HHVM_VERSION')) {
flush();
}
exit(1);
}
}
throw $exception;
}
return false;
}
这里需要注意的是,如果__toString内的非error异常,是无法再throw的,有兴趣的朋友可以试一下如下代码
set_error_handler(function(){
throw new Exception("exception");
});
set_exception_handler(function($e){
var_dump($e);
});
class obj{
public function __toString(){
echo $name; //模拟一个notice异常
return "string";
}
}
$obj = new obj;
echo $obj;
yii是通过debug_backtrace函数获取调用栈,来判断是否是因为toString函数造成的异常,我感觉这块非常影响效率
handlerException用来捕获handlerError抛出来的异常,和PHP7中的error异常
public function handleException($exception)
{
if ($exception instanceof ExitException) {
return;
}
$this->exception = $exception;
// disable error capturing to avoid recursive errors while handling exceptions
$this->unregister(); //回收异常函数
// set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent
// HTTP exceptions will override this value in renderException()
if (PHP_SAPI !== 'cli') {
http_response_code(500); //如果不是cli模式,就响应500
}
try {
$this->logException($exception); //记录日志
if ($this->discardExistingOutput) {
$this->clearOutput(); //清除所有输出层,只输出异常信息
}
$this->renderException($exception);
if (!YII_ENV_TEST) {
\Yii::getLogger()->flush(true);
if (defined('HHVM_VERSION')) {
flush();
}
exit(1);
}
} 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);
}
$this->exception = null;
}
renderException方法是核心
protected function renderException($exception)
{
if (Yii::$app->has('response')) { //是否已经加载了response组件,在实例化Application过程中会加载
$response = Yii::$app->getResponse();
// reset parameters of response to avoid interference with partially created response data
// in case the error occurred while sending the response.
$response->isSent = false;
$response->stream = null;
$response->data = null;
$response->content = null;
} else {
$response = new Response();
}
$response->setStatusCodeByException($exception); //设置响应信息文字,不是设置httpcode,而是响应header
$useErrorView = $response->format === Response::FORMAT_HTML && (!YII_DEBUG || $exception instanceof UserException);//如果响应格式是html并且(不是debug模式或者是UserException异常)
if ($useErrorView && $this->errorAction !== null) {
$result = Yii::$app->runAction($this->errorAction); //调用errorAction,一般都是调用一个用户响应异常的控制器方法
if ($result instanceof Response) {
$response = $result;
} else {
$response->data = $result;
}
} elseif ($response->format === Response::FORMAT_HTML) {
if ($this->shouldRenderSimpleHtml()) { //如果是开发模式或者是ajax响应
// AJAX request
$response->data = '<pre>' . $this->htmlEncode(static::convertExceptionToString($exception)) . '</pre>';
} else {
// if there is an error during error rendering it's useful to
// display PHP error in debug mode instead of a blank screen
if (YII_DEBUG) { //如果是debug模式就显示带大红框的异常信息
ini_set('display_errors', 1);
}
$file = $useErrorView ? $this->errorView : $this->exceptionView;
$response->data = $this->renderFile($file, [ //加载异常页面
'exception' => $exception,
]);
}
} elseif ($response->format === Response::FORMAT_RAW) {
$response->data = static::convertExceptionToString($exception);
} else {
$response->data = $this->convertExceptionToArray($exception);
}
$response->send();
}
还有一个shutdown函数,因为在PHP5版本中,error级别的异常是无法被set_exception_handler函数捕获的,只能通过注册一个shutdown函数来用error_get_last来获取error异常
public function handleFatalError()
{
unset($this->_memoryReserve);
// load ErrorException manually here because autoloading them will not work
// when error occurs while autoloading a class
if (!class_exists('yii\\base\\ErrorException', false)) {
require_once __DIR__ . '/ErrorException.php';
}
$error = error_get_last();
if (ErrorException::isFatalError($error)) {
if (!empty($this->_hhvmException)) {
$exception = $this->_hhvmException;
} else {
$exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']);
}
$this->exception = $exception;
$this->logException($exception);
if ($this->discardExistingOutput) {
$this->clearOutput();
}
$this->renderException($exception);
// need to explicitly flush logs because exit() next will terminate the app immediately
Yii::getLogger()->flush(true);
if (defined('HHVM_VERSION')) {
flush();
}
exit(1);
}
}