Skip to content

Latest commit

 

History

History
247 lines (227 loc) · 9.69 KB

[请求处理三]错误处理源码.md

File metadata and controls

247 lines (227 loc) · 9.69 KB

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);
    }
}