Yii2 异常及错误处理


异常及错误处理


正文

errorHandler组件调用Yii::error()Yii::error()调用yii\log\Loggerlog()方法追加日志, yii\log\Loggerflush()方法调用yii\log\Dispatcherdispatch()方法分发日志, yii\log\Dispatcherdispatch()方法调用 target 的collect()方法收集日志, target 的collect()方法再调用自己的export()方法把日志持久化到储存中。

可以看到里面有三个角色:

  1. 错误异常处理组件;
  2. 日志组件,包括日志分发操作;
  3. 日志持久化组件;

异常及错误处理程序注册

在config配置文件中写入异常及错误处理的配置:

"components" => [
    ...
    'errorHandler'  =>  [
        // 自定义异常及错误处理类
        'class' => 'common\components\LErrorHandler',
    ],
    ...
],

自定义异常及错误处理类,web的继承自yii\web\ErrorHandler,console的继承自yii\console\ErrorHandler,这两个又都继承自\yii\base\ErrorHandler, 而\yii\base\ErrorHandler又继承自yii\base\Component。

运行项目入口文件index.php中有:

$config = yii\helpers\ArrayHelper::merge(
    require(__DIR__ . '/../../common/config/main.php'),
    require(__DIR__ . '/../../common/config/main-prod.php'),
    require(__DIR__ . '/../config/main.php'),
    require(__DIR__ . '/../config/main-prod.php')
);

$application = new yii\web\Application($config);
$application->run();

yii\web\Application实例化会调用父类yii\base\Application中的 __construct($config = [])

public function __construct($config = [])
{
    Yii::$app = $this;
    static::setInstance($this);

    $this->state = self::STATE_BEGIN;

    // 会让我们写在配置文件中的errorHandler替代yii\web\Application中写在coreComponents()的 'errorHandler' => ['class' => 'yii\web\ErrorHandler'
    $this->preInit($config);

    // 异常及错误处理程序注册代码
    $this->registerErrorHandler($config);

    Component::__construct($config);
}

这里可以看到异常及错误处理程序注册相关代码:

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);
        }
        
        // 向服务定位器注册 errorHandler 服务,这里属于提前注册,因为getErrorHandler()中get('errorHandler')就属于获取服务了,
        // 而批量注册服务,要等到下一步运行 Component::__construct($config),所以这里需要提前注册
        $this->set('errorHandler', $config['components']['errorHandler']);
        
        // 上面已经注册errorHandler服务,批量注册服务阶段没必要再次注册,所以取消这个配置参数
        unset($config['components']['errorHandler']);
        
        $this->getErrorHandler()->register();
    }
}

public function getErrorHandler()
{
    return $this->get('errorHandler');
}

register()写在yii\base\ErrorHandler中:

public function register()
{
    // 页面不显示错误
    ini_set('display_errors', false);
    
    // 设置用户自定义的异常处理函数,可以在自己写的继承类中重载handleException()方法
    set_exception_handler([$this, 'handleException']);
    
    // 设置用户自定义的错误处理函数,可以在自己写的继承类中重载
    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);
    }
    
    // 设置用户自定义的PHP程序执行完成后(包括致命错误导致的程序执行完成)执行的函数,可以在自己写的继承类中重载handleFatalError()方法
    register_shutdown_function([$this, 'handleFatalError']);
}

public function unregister()
{
    // 恢复之前定义过的异常处理函数
    restore_error_handler();
    
    // 还原之前的错误处理函数
    restore_exception_handler();
}

这样下来就算是注册好了异常及错误处理程序了。

异常及错误写入日志分析

既然发现了异常和错误,那我们就要对异常和错误可监控,最好的方式是把异常及错误写入日志,以备查询。

我们看到在yii\base\ErrorHandler的handleException()、handleError()、handleFatalError()等异常及错误处理程序中都有$this->logException($exception)这一句:

public function logException($exception)
{
    $category = get_class($exception);
    if ($exception instanceof HttpException) {
        $category = 'yii\\web\\HttpException:' . $exception->statusCode;
    } elseif ($exception instanceof \ErrorException) {/**/
        $category .= ':' . $exception->getSeverity();
    }
    Yii::error($exception, $category);
}

Yii::error()写在yii\BaseYii工具类中的方法:

public static function error($message, $category = 'application')
{
    static::getLogger()->log($message, Logger::LEVEL_ERROR, $category);
}

public static function getLogger()
{
    if (self::$_logger !== null) {
        return self::$_logger;
    } else {
        return self::$_logger = static::createObject('yii\log\Logger');
    }
}

这里使用日志服务的log()方法把异常和错误写入日志。

异常及错误输出控制

写入日志后,现在还有一个问题没有解决,就是程序怎么输出的问题,要不要把错误打印出来,怎么显示这个错误。

我们看一下上面我们使用的自定义的异常及错误处理类代码,找找感觉。

common\components\LErrorHandler:

<?php
namespace common\components;

use common\misc\LError;
use Yii;
use yii\base\ErrorException;
use yii\web\Application;
use yii\web\ErrorHandler;
use yii\web\HttpException;

class LErrorHandler extends ErrorHandler
{
    public function handleException($exception)
    {
        $data = $this->formatException($exception);
        /** @var $app Application */
        $app = Yii::$app;
        /** @var $controller LController */
        $controller = $app->controller;
        if (!$controller instanceof LController) {
            $controller = $app->createController('site');

            $controller = $controller[0];
        }
        $this->logException($exception);
        $controller->ajaxReturn(
            isset($data['errorCode']) ? $data['errorCode'] : $data['code'] ? $data['code'] : 500, 
            ENV_DEBUG ? $data['message'] : array(), 
            ENV_DEBUG ? $data : array()
        );
    }

    public function handleError($code, $message, $file, $line)
    {
        /** @var $app Application */
        $app = Yii::$app;
        /** @var $controller LController */
        $controller = $app->controller;
        if (!$controller instanceof LController) {
            $controller = $app->createController('/site');
            $controller = $controller[0];
        }
        $exception = new \ErrorException($message, $code, 1, $file, $line);
        $this->logException($exception);
        $data = ["msg" => "file:" . $file . ",line:" . $line];
        $controller->ajaxReturn(LError::INTERNAL_ERROR, ENV_DEBUG ? $message : array(), ENV_DEBUG ? $data : array());
    }

    public function handleFatalError()
    {
        $error = error_get_last();
        if (ErrorException::isFatalError($error)) {
            $exception = new \ErrorException($error['message'], 500, $error['type'], $error['file'], $error['line']);
            $this->exception = $exception;
            $this->logException($exception);

            if ($this->discardExistingOutput) {
                $this->clearOutput();
            }
            // need to explicitly flush logs because exit() next will terminate the app immediately
            Yii::getLogger()->flush(true);
            $this->renderException($exception);
        }
    }
    
    public function renderException($exception)
    {
        $this->handleException($exception);
    }
    
    protected function formatException($exception)
    {
        $fileName = $exception->getFile();
        $errorLine = $exception->getLine();

        $trace = $exception->getTrace();

        foreach ($trace as $i => $t) {
            if (!isset($t['file'])) {
                $trace[$i]['file'] = 'unknown';
            }

            if (!isset($t['line'])) {
                $trace[$i]['line'] = 0;
            }

            if (!isset($t['function'])) {
                $trace[$i]['function'] = 'unknown';
            }

            unset($trace[$i]['object']);
        }

        return array(
            'code' => ($exception instanceof HttpException) ? $exception->statusCode : 500,
            'type' => get_class($exception),
            'errorCode' => $exception->getCode(),
            'message' => $exception->getMessage(),
            'file' => $fileName,
            'line' => $errorLine,
            'trace' => $exception->getTraceAsString(),
            'traces' => $trace,
        );
    }
}

ENV_DEBUG是DEBUG环境的定义,非生产环境可以在config配置文件中定义为true:

defined('ENV_DEBUG') or define("ENV_DEBUG", true);

控制器基类common\components\LController:

<?php
namespace common\components;

use common\misc\LError;
use common\service\LLogRequestBlackListService;
use stdClass;
use Yii;
use yii\log\Logger;
use yii\web\Controller;
use yii\web\Response;

class LController extends Controller
{
    public function beforeAction( $action )
    {
        // 修改预请求返回参数
        if (Yii::$app->request->getIsOptions()) {
            $result = [
                'code' => 200,
                'msg' => 'success'
            ];
            Yii::$app->response->format = Response::FORMAT_JSON;
            Yii::$app->response->data = $result;
            return;
        }
        
        // 白名单,黑名单 过滤
        ...
        
        return parent::beforeAction($action);
    }
    
    public function ajaxResponse($result = array())
    {
        /** @var LHttpRequest $request */
        $request = Yii::$app->request;
        /** @var Response $response */
        $response = Yii::$app->response;
        $callback = $request->get('callback');
        if (empty($result)) {
            $result = new stdClass();
        }

        if ($callback && is_string($callback) && preg_match('/^[0-9A-Za-z_]+$/', $callback)) {
            $response->format = Response::FORMAT_JSONP;
            $response->content = 'try{' . $callback . '(' . json_encode($result) . ');}catch(e){}';
        } else {
            $response->format = Response::FORMAT_JSON;
            $response->content = json_encode($result, JSON_UNESCAPED_UNICODE);
        }
        
        // 输出写入日志
        $pathInfo = $request->getPathInfo();
        if (!LLogRequestBlackListService::inBlackList($pathInfo)) {
            $context['actionUrl'] = Yii::$app->request->getUrl();
            $context['result'] = $result;
            $context['requestType'] = 'output';
            $context["code_time"] = (microtime(true) - YII_BEGIN_TIME) * 1000;      // 返回耗时
            if ($context["result"]["data"]) {
                $context["result"]["data"] = json_encode($context["result"]["data"], JSON_UNESCAPED_UNICODE);
            }
            Yii::getLogger()->log($context, Logger::LEVEL_TRACE, "application");
        }
       
        // 返回,结束程序运行
        Yii::$app->end(0, $response);
    }
    
    public function ajaxSuccess(array $data = array())
    {
        $this->ajaxReturn(LError::SUCCESS, '', $data);
    }
    
    public function ajaxReturn($code = LError::SUCCESS, $msg = array(), $data = null)
    {
        if (is_array($msg) || !$msg) {
            $msg = LError::getErrMsgByCode($code, $msg);
        }
        if (is_null($data)) {
            $data = new stdClass();
        } else if (!$data) {
            $data = [];
        }
        
        $this->ajaxResponse(array(
            'code' => $code,
            'msg' => $msg,
            'data' => $data,
        ));
    }
}

common\misc\LError:

<?php
namespace common\misc;

class LError
{
    const SUCCESS = 200;
    const FAILURE = 500;
    /** 内部错误 **/ 
    const INTERNAL_ERROR = 100001001;   
    /** 登录参数异常 */
    const LOGIN_PARAM_ERROR = 100002001;
    /** 身份验证失败 */
    const AUTH_TEST_ERROR = 100002002;
    /** 权限不足 */
    const AUTH_NOT_ENOUGH = 100002003;
    ...以及其他...(可以把错误码分三块,前三位表示是哪个系统、中间三位表示是系统中哪个模块、后三位具体表示是模块的什么错误)
    
    public static $errMsg = [
        self::SUCCESS => '成功',
        self::INTERNAL_ERROR => "系统错误",
        self::LOGIN_PARAM_ERROR => "登录参数异常",
        self::AUTH_TEST_ERROR => "身份验证失败",
        self::AUTH_NOT_ENOUGH => "权限不足",
    ];
    
    public static function getErrMsg($message, array $params = array())
    {
        $patterns = array_map(function($pattern) {
            return "/#$pattern#/";
        }, array_keys($params));
        $values = array_values($params);
        
        return preg_replace($patterns, $values, $message);
    }
    
    public static function getErrMsgByCode($code, array $params = array())
    {
        $errMsg = static::errorMsg();
        $message = isset($errMsg[$code]) ? $errMsg[$code] : '服务器忙,请稍后再试~';
        
        return self::getErrMsg($message, $params);
    }
    
    public static function errorMsg()
    {
        return self::$errMsg;
    }
    
    /**
     * 合并错误码数组,保证第一个数组会被后面的数组覆盖
     * @param array $errMsg
     * @param array $extendMsg
     * @return array
     */
    public static function mergeErrorMsg($errMsg, $extendMsg)
    {
        $args = func_get_args();
        $res = array_shift($args);
        while (!empty($args)) {
            $next = array_shift($args);
            foreach ($next as $k => $v) {
                if (is_array($v) && isset($res[$k]) && is_array($res[$k]))
                    $res[$k] = self::mergeErrorMsg($res[$k], $v);
                else
                    $res[$k] = $v;
            }
        }
        
        return $res;
    }
    
    public static function getReturn($code = LError::SUCCESS, $msg = array(), $data = null)
    {
        if (is_array($msg) || !$msg) {
            $msg = self::getErrMsgByCode($code, $msg);
        }
        if (is_null($data)) {
            $data = new \stdClass();
        } else if (!$data) {
            $data = [];
        }
        return [
            'code' => $code,
            'msg' => $msg,
            'data' => $data,
        ];
    }
    
    public static function isSuccess($response)
    {
        if (isset($response['code']) && $response['code'] == self::SUCCESS) {
            return true;
        } else {
            return false;
        }
    }
}

common\service\LLogRequestBlackListService:

<?php
namespace common\service;

use Yii;

class LLogRequestBlackListService
{
    /**
     * 是否在请求记录的黑名单中
     * @param $url
     * @return bool
     */
    public static function inBlackList($url)
    {
        $blackList = Yii::$app->params['logBlackList'];
        return in_array($url, $blackList) ? true : false;
    }
}

源码

yii\base\ErrorHandler

yii\base\ErrorHandler 是对错误及异常相关的处理类,在应用注册时会注册错误异常处理方法,就是注册的这里的方法。 我们可以继承这个类,针对错误异常处理做定制。 当抛出异常、触发错误、致命错误时,这里决定程序怎么处理错误及异常,一般是调用Yii::error()方法(迪米特法则(LOD))去做后续处理(写入日志)。 我们拓展这个类的功能时,可以把错误及异常发生后程序向页面输出什么做一下定制。

<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace yii\base;

use Yii;
use yii\helpers\VarDumper;
use yii\web\HttpException;

/**
 * ErrorHandler handles uncaught PHP errors and exceptions.
 *
 * ErrorHandler is configured as an application component in [[\yii\base\Application]] by default.
 * You can access that instance via `Yii::$app->errorHandler`.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @author Alexander Makarov <sam@rmcreative.ru>
 * @author Carsten Brandt <mail@cebe.cc>
 * @since 2.0
 */
abstract class ErrorHandler extends Component
{
    /**
     * @var boolean whether to discard any existing page output before error display. Defaults to true.
     */
    public $discardExistingOutput = true;
    /**
     * @var integer the size of the reserved memory. A portion of memory is pre-allocated so that
     * when an out-of-memory issue occurs, the error handler is able to handle the error with
     * the help of this reserved memory. If you set this value to be 0, no memory will be reserved.
     * Defaults to 256KB.
     */
    public $memoryReserveSize = 262144;
    /**
     * @var \Exception the exception that is being handled currently.
     */
    public $exception;

    /**
     * @var string Used to reserve memory for fatal error handler.
     */
    private $_memoryReserve;
    /**
     * @var \Exception from HHVM error that stores backtrace
     */
    private $_hhvmException;


    /**
     * Register this error handler
     */
    public function register()
    {
//        ini_set('display_errors', false);
        set_exception_handler([$this, 'handleException']);
        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']);
    }

    /**
     * Unregisters this error handler by restoring the PHP error and exception handlers.
     */
    public function unregister()
    {
        restore_error_handler();
        restore_exception_handler();
    }

    /**
     * Handles uncaught PHP exceptions.
     *
     * This method is implemented as a PHP exception handler.
     *
     * @param \Exception $exception the exception that is not caught
     */
    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);
        }

        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
            $msg = "An Error occurred while handling another error:\n";
            $msg .= (string) $e;
            $msg .= "\nPrevious exception:\n";
            $msg .= (string) $exception;
            if (YII_DEBUG) {
                if (PHP_SAPI === 'cli') {
                    echo $msg . "\n";
                } else {
                    echo '<pre>' . htmlspecialchars($msg, ENT_QUOTES, Yii::$app->charset) . '</pre>';
                }
            } else {
                echo 'An internal server error occurred.';
            }
            $msg .= "\n\$_SERVER = " . VarDumper::export($_SERVER);
            error_log($msg);
            if (defined('HHVM_VERSION')) {
                flush();
            }
            exit(1);
        }

        $this->exception = null;
    }

    /**
     * Handles HHVM execution errors such as warnings and notices.
     *
     * This method is used as a HHVM error handler. It will store exception that will
     * be used in fatal error handler
     *
     * @param integer $code the level of the error raised.
     * @param string $message the error message.
     * @param string $file the filename that the error was raised in.
     * @param integer $line the line number the error was raised at.
     * @param mixed $context
     * @param mixed $backtrace trace of error
     * @return boolean whether the normal error handler continues.
     *
     * @throws ErrorException
     * @since 2.0.6
     */
    public function handleHhvmError($code, $message, $file, $line, $context, $backtrace)
    {
        if ($this->handleError($code, $message, $file, $line)) {
            return true;
        }
        if (E_ERROR & $code) {
            $exception = new ErrorException($message, $code, $code, $file, $line);
            $ref = new \ReflectionProperty('\Exception', 'trace');
            $ref->setAccessible(true);
            $ref->setValue($exception, $backtrace);
            $this->_hhvmException = $exception;
        }
        return false;
    }

    /**
     * Handles PHP execution errors such as warnings and notices.
     *
     * This method is used as a PHP error handler. It will simply raise an [[ErrorException]].
     *
     * @param integer $code the level of the error raised.
     * @param string $message the error message.
     * @param string $file the filename that the error was raised in.
     * @param integer $line the line number the error was raised at.
     * @return boolean whether the normal error handler continues.
     *
     * @throws ErrorException
     */
    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;
    }

    /**
     * Handles fatal PHP errors
     */
    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);
        }
    }

    /**
     * Renders the exception.
     * @param \Exception $exception the exception to be rendered.
     */
    abstract protected function renderException($exception);

    /**
     * Logs the given exception
     * @param \Exception $exception the exception to be logged
     * @since 2.0.3 this method is now public.
     */
    public function logException($exception)
    {
        $category = get_class($exception);
        if ($exception instanceof HttpException) {
            $category = 'yii\\web\\HttpException:' . $exception->statusCode;
        } elseif ($exception instanceof \ErrorException) {/**/
            $category .= ':' . $exception->getSeverity();
        }
        Yii::error($exception, $category);
    }

    /**
     * Removes all output echoed before calling this method.
     */
    public function clearOutput()
    {
        // the following manual level counting is to deal with zlib.output_compression set to On
        for ($level = ob_get_level(); $level > 0; --$level) {
            if (!@ob_end_clean()) {
                ob_clean();
            }
        }
    }

    /**
     * Converts an exception into a PHP error.
     *
     * This method can be used to convert exceptions inside of methods like `__toString()`
     * to PHP errors because exceptions cannot be thrown inside of them.
     * @param \Exception $exception the exception to convert to a PHP error.
     */
    public static function convertExceptionToError($exception)
    {
        trigger_error(static::convertExceptionToString($exception), E_USER_ERROR);
    }

    /**
     * Converts an exception into a simple string.
     * @param \Exception $exception the exception being converted
     * @return string the string representation of the exception.
     */
    public static function convertExceptionToString($exception)
    {
        if ($exception instanceof Exception && ($exception instanceof UserException || !YII_DEBUG)) {
            $message = "{$exception->getName()}: {$exception->getMessage()}";
        } elseif (YII_DEBUG) {
            if ($exception instanceof Exception) {
                $message = "Exception ({$exception->getName()})";
            } elseif ($exception instanceof ErrorException) {
                $message = "{$exception->getName()}";
            } else {
                $message = 'Exception';
            }
            $message .= " '" . get_class($exception) . "' with message '{$exception->getMessage()}' \n\nin "
                . $exception->getFile() . ':' . $exception->getLine() . "\n\n"
                . "Stack trace:\n" . $exception->getTraceAsString();
        } else {
            $message = 'Error: ' . $exception->getMessage();
        }
        return $message;
    }
}
yii\web\ErrorHandler

yii\web\ErrorHandler 继承自 yii\base\ErrorHandler ,是web访问时的错误异常处理做定制拓展:

<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace yii\web;

use Yii;
use yii\base\Exception;
use yii\base\ErrorException;
use yii\base\UserException;
use yii\helpers\VarDumper;

/**
 * ErrorHandler handles uncaught PHP errors and exceptions.
 *
 * ErrorHandler displays these errors using appropriate views based on the
 * nature of the errors and the mode the application runs at.
 *
 * ErrorHandler is configured as an application component in [[\yii\base\Application]] by default.
 * You can access that instance via `Yii::$app->errorHandler`.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @author Timur Ruziev <resurtm@gmail.com>
 * @since 2.0
 */
class ErrorHandler extends \yii\base\ErrorHandler
{
    /**
     * @var integer maximum number of source code lines to be displayed. Defaults to 19.
     */
    public $maxSourceLines = 19;
    /**
     * @var integer maximum number of trace source code lines to be displayed. Defaults to 13.
     */
    public $maxTraceSourceLines = 13;
    /**
     * @var string the route (e.g. 'site/error') to the controller action that will be used
     * to display external errors. Inside the action, it can retrieve the error information
     * using `Yii::$app->errorHandler->exception. This property defaults to null, meaning ErrorHandler
     * will handle the error display.
     */
    public $errorAction;
    /**
     * @var string the path of the view file for rendering exceptions without call stack information.
     */
    public $errorView = '@yii/views/errorHandler/error.php';
    /**
     * @var string the path of the view file for rendering exceptions.
     */
    public $exceptionView = '@yii/views/errorHandler/exception.php';
    /**
     * @var string the path of the view file for rendering exceptions and errors call stack element.
     */
    public $callStackItemView = '@yii/views/errorHandler/callStackItem.php';
    /**
     * @var string the path of the view file for rendering previous exceptions.
     */
    public $previousExceptionView = '@yii/views/errorHandler/previousException.php';
    /**
     * @var array list of the PHP predefined variables that should be displayed on the error page.
     * Note that a variable must be accessible via `$GLOBALS`. Otherwise it won't be displayed.
     * Defaults to `['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION']`.
     * @see renderRequest()
     * @since 2.0.7
     */
    public $displayVars = ['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION'];


    /**
     * Renders the exception.
     * @param \Exception $exception the exception to be rendered.
     */
    protected function renderException($exception)
    {
        if (Yii::$app->has('response')) {
            $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();
        }

        $useErrorView = $response->format === Response::FORMAT_HTML && (!YII_DEBUG || $exception instanceof UserException);

        if ($useErrorView && $this->errorAction !== null) {
            $result = Yii::$app->runAction($this->errorAction);
            if ($result instanceof Response) {
                $response = $result;
            } else {
                $response->data = $result;
            }
        } elseif ($response->format === Response::FORMAT_HTML) {
            if (YII_ENV_TEST || isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
                // 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) {
                    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);
        }

        if ($exception instanceof HttpException) {
            $response->setStatusCode($exception->statusCode);
        } else {
            $response->setStatusCode(500);
        }

        $response->send();
    }

    /**
     * Converts an exception into an array.
     * @param \Exception $exception the exception being converted
     * @return array the array representation of the exception.
     */
    protected function convertExceptionToArray($exception)
    {
        if (!YII_DEBUG && !$exception instanceof UserException && !$exception instanceof HttpException) {
            $exception = new HttpException(500, Yii::t('yii', 'An internal server error occurred.'));
        }

        $array = [
            'name' => ($exception instanceof Exception || $exception instanceof ErrorException) ? $exception->getName() : 'Exception',
            'message' => $exception->getMessage(),
            'code' => $exception->getCode(),
        ];
        if ($exception instanceof HttpException) {
            $array['status'] = $exception->statusCode;
        }
        if (YII_DEBUG) {
            $array['type'] = get_class($exception);
            if (!$exception instanceof UserException) {
                $array['file'] = $exception->getFile();
                $array['line'] = $exception->getLine();
                $array['stack-trace'] = explode("\n", $exception->getTraceAsString());
                if ($exception instanceof \yii\db\Exception) {
                    $array['error-info'] = $exception->errorInfo;
                }
            }
        }
        if (($prev = $exception->getPrevious()) !== null) {
            $array['previous'] = $this->convertExceptionToArray($prev);
        }

        return $array;
    }

    /**
     * Converts special characters to HTML entities.
     * @param string $text to encode.
     * @return string encoded original text.
     */
    public function htmlEncode($text)
    {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }

    /**
     * Adds informational links to the given PHP type/class.
     * @param string $code type/class name to be linkified.
     * @return string linkified with HTML type/class name.
     */
    public function addTypeLinks($code)
    {
        if (preg_match('/(.*?)::([^(]+)/', $code, $matches)) {
            $class = $matches[1];
            $method = $matches[2];
            $text = $this->htmlEncode($class) . '::' . $this->htmlEncode($method);
        } else {
            $class = $code;
            $method = null;
            $text = $this->htmlEncode($class);
        }

        $url = $this->getTypeUrl($class, $method);

        if (!$url) {
            return $text;
        }

        return '<a href="' . $url . '" target="_blank">' . $text . '</a>';
    }

    /**
     * Returns the informational link URL for a given PHP type/class.
     * @param string $class the type or class name.
     * @param string|null $method the method name.
     * @return string|null the informational link URL.
     * @see addTypeLinks()
     */
    protected function getTypeUrl($class, $method)
    {
        if (strpos($class, 'yii\\') !== 0) {
            return null;
        }

        $page = $this->htmlEncode(strtolower(str_replace('\\', '-', $class)));
        $url = "http://www.yiiframework.com/doc-2.0/$page.html";
        if ($method) {
            $url .= "#$method()-detail";
        }

        return $url;
    }

    /**
     * Renders a view file as a PHP script.
     * @param string $_file_ the view file.
     * @param array $_params_ the parameters (name-value pairs) that will be extracted and made available in the view file.
     * @return string the rendering result
     */
    public function renderFile($_file_, $_params_)
    {
        $_params_['handler'] = $this;
        if ($this->exception instanceof ErrorException || !Yii::$app->has('view')) {
            ob_start();
            ob_implicit_flush(false);
            extract($_params_, EXTR_OVERWRITE);
            require(Yii::getAlias($_file_));

            return ob_get_clean();
        } else {
            return Yii::$app->getView()->renderFile($_file_, $_params_, $this);
        }
    }

    /**
     * Renders the previous exception stack for a given Exception.
     * @param \Exception $exception the exception whose precursors should be rendered.
     * @return string HTML content of the rendered previous exceptions.
     * Empty string if there are none.
     */
    public function renderPreviousExceptions($exception)
    {
        if (($previous = $exception->getPrevious()) !== null) {
            return $this->renderFile($this->previousExceptionView, ['exception' => $previous]);
        } else {
            return '';
        }
    }

    /**
     * Renders a single call stack element.
     * @param string|null $file name where call has happened.
     * @param integer|null $line number on which call has happened.
     * @param string|null $class called class name.
     * @param string|null $method called function/method name.
     * @param array $args array of method arguments.
     * @param integer $index number of the call stack element.
     * @return string HTML content of the rendered call stack element.
     */
    public function renderCallStackItem($file, $line, $class, $method, $args, $index)
    {
        $lines = [];
        $begin = $end = 0;
        if ($file !== null && $line !== null) {
            $line--; // adjust line number from one-based to zero-based
            $lines = @file($file);
            if ($line < 0 || $lines === false || ($lineCount = count($lines)) < $line) {
                return '';
            }

            $half = (int) (($index === 1 ? $this->maxSourceLines : $this->maxTraceSourceLines) / 2);
            $begin = $line - $half > 0 ? $line - $half : 0;
            $end = $line + $half < $lineCount ? $line + $half : $lineCount - 1;
        }

        return $this->renderFile($this->callStackItemView, [
            'file' => $file,
            'line' => $line,
            'class' => $class,
            'method' => $method,
            'index' => $index,
            'lines' => $lines,
            'begin' => $begin,
            'end' => $end,
            'args' => $args,
        ]);
    }

    /**
     * Renders the global variables of the request.
     * List of global variables is defined in [[displayVars]].
     * @return string the rendering result
     * @see displayVars
     */
    public function renderRequest()
    {
        $request = '';
        foreach ($this->displayVars as $name) {
            if (!empty($GLOBALS[$name])) {
                $request .= '$' . $name . ' = ' . VarDumper::export($GLOBALS[$name]) . ";\n\n";
            }
        }

        return '<pre>' . rtrim($request, "\n") . '</pre>';
    }

    /**
     * Determines whether given name of the file belongs to the framework.
     * @param string $file name to be checked.
     * @return boolean whether given name of the file belongs to the framework.
     */
    public function isCoreFile($file)
    {
        return $file === null || strpos(realpath($file), YII2_PATH . DIRECTORY_SEPARATOR) === 0;
    }

    /**
     * Creates HTML containing link to the page with the information on given HTTP status code.
     * @param integer $statusCode to be used to generate information link.
     * @param string $statusDescription Description to display after the the status code.
     * @return string generated HTML with HTTP status code information.
     */
    public function createHttpStatusLink($statusCode, $statusDescription)
    {
        return '<a href="http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#' . (int) $statusCode . '" target="_blank">HTTP ' . (int) $statusCode . ' &ndash; ' . $statusDescription . '</a>';
    }

    /**
     * Creates string containing HTML link which refers to the home page of determined web-server software
     * and its full name.
     * @return string server software information hyperlink.
     */
    public function createServerInformationLink()
    {
        $serverUrls = [
            'http://httpd.apache.org/' => ['apache'],
            'http://nginx.org/' => ['nginx'],
            'http://lighttpd.net/' => ['lighttpd'],
            'http://gwan.com/' => ['g-wan', 'gwan'],
            'http://iis.net/' => ['iis', 'services'],
            'http://php.net/manual/en/features.commandline.webserver.php' => ['development'],
        ];
        if (isset($_SERVER['SERVER_SOFTWARE'])) {
            foreach ($serverUrls as $url => $keywords) {
                foreach ($keywords as $keyword) {
                    if (stripos($_SERVER['SERVER_SOFTWARE'], $keyword) !== false) {
                        return '<a href="' . $url . '" target="_blank">' . $this->htmlEncode($_SERVER['SERVER_SOFTWARE']) . '</a>';
                    }
                }
            }
        }

        return '';
    }

    /**
     * Creates string containing HTML link which refers to the page with the current version
     * of the framework and version number text.
     * @return string framework version information hyperlink.
     */
    public function createFrameworkVersionLink()
    {
        return '<a href="http://github.com/yiisoft/yii2/" target="_blank">' . $this->htmlEncode(Yii::getVersion()) . '</a>';
    }

    /**
     * Converts arguments array to its string representation
     *
     * @param array $args arguments array to be converted
     * @return string string representation of the arguments array
     */
    public function argumentsToString($args)
    {
        $count = 0;
        $isAssoc = $args !== array_values($args);

        foreach ($args as $key => $value) {
            $count++;
            if ($count>=5) {
                if ($count>5) {
                    unset($args[$key]);
                } else {
                    $args[$key] = '...';
                }
                continue;
            }

            if (is_object($value)) {
                $args[$key] = '<span class="title">' . $this->htmlEncode(get_class($value)) . '</span>';
            } elseif (is_bool($value)) {
                $args[$key] = '<span class="keyword">' . ($value ? 'true' : 'false') . '</span>';
            } elseif (is_string($value)) {
                $fullValue = $this->htmlEncode($value);
                if (mb_strlen($value, 'UTF-8') > 32) {
                    $displayValue = $this->htmlEncode(mb_substr($value, 0, 32, 'UTF-8')) . '...';
                    $args[$key] = "<span class=\"string\" title=\"$fullValue\">'$displayValue'</span>";
                } else {
                    $args[$key] = "<span class=\"string\">'$fullValue'</span>";
                }
            } elseif (is_array($value)) {
                $args[$key] = '[' . $this->argumentsToString($value) . ']';
            } elseif ($value === null) {
                $args[$key] = '<span class="keyword">null</span>';
            } elseif (is_resource($value)) {
                $args[$key] = '<span class="keyword">resource</span>';
            } else {
                $args[$key] = '<span class="number">' . $value . '</span>';
            }

            if (is_string($key)) {
                $args[$key] = '<span class="string">\'' . $this->htmlEncode($key) . "'</span> => $args[$key]";
            } elseif ($isAssoc) {
                $args[$key] = "<span class=\"number\">$key</span> => $args[$key]";
            }
        }
        $out = implode(', ', $args);

        return $out;
    }

    /**
     * Returns human-readable exception name
     * @param \Exception $exception
     * @return string human-readable exception name or null if it cannot be determined
     */
    public function getExceptionName($exception)
    {
        if ($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\InvalidCallException || $exception instanceof \yii\base\InvalidParamException || $exception instanceof \yii\base\UnknownMethodException) {
            return $exception->getName();
        }
        return null;
    }
}
yii\console\ErrorHandler

yii\console\ErrorHandler 继承自 yii\base\ErrorHandler ,是cli访问时的错误异常处理做定制拓展:

<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace yii\console;

use Yii;
use yii\base\ErrorException;
use yii\base\UserException;
use yii\helpers\Console;

/**
 * ErrorHandler handles uncaught PHP errors and exceptions.
 *
 * ErrorHandler is configured as an application component in [[\yii\base\Application]] by default.
 * You can access that instance via `Yii::$app->errorHandler`.
 *
 * @author Carsten Brandt <mail@cebe.cc>
 * @since 2.0
 */
class ErrorHandler extends \yii\base\ErrorHandler
{
    /**
     * Renders an exception using ansi format for console output.
     * @param \Exception $exception the exception to be rendered.
     */
    protected function renderException($exception)
    {
        if ($exception instanceof Exception && ($exception instanceof UserException || !YII_DEBUG)) {
            $message = $this->formatMessage($exception->getName() . ': ') . $exception->getMessage();
        } elseif (YII_DEBUG) {
            if ($exception instanceof Exception) {
                $message = $this->formatMessage("Exception ({$exception->getName()})");
            } elseif ($exception instanceof ErrorException) {
                $message = $this->formatMessage($exception->getName());
            } else {
                $message = $this->formatMessage('Exception');
            }
            $message .= $this->formatMessage(" '" . get_class($exception) . "'", [Console::BOLD, Console::FG_BLUE])
                . ' with message ' . $this->formatMessage("'{$exception->getMessage()}'", [Console::BOLD]) //. "\n"
                . "\n\nin " . dirname($exception->getFile()) . DIRECTORY_SEPARATOR . $this->formatMessage(basename($exception->getFile()), [Console::BOLD])
                . ':' . $this->formatMessage($exception->getLine(), [Console::BOLD, Console::FG_YELLOW]) . "\n";
            if ($exception instanceof \yii\db\Exception && !empty($exception->errorInfo)) {
                $message .= "\n" . $this->formatMessage("Error Info:\n", [Console::BOLD]) . print_r($exception->errorInfo, true);
            }
            $message .= "\n" . $this->formatMessage("Stack trace:\n", [Console::BOLD]) . $exception->getTraceAsString();
        } else {
            $message = $this->formatMessage('Error: ') . $exception->getMessage();
        }

        if (PHP_SAPI === 'cli') {
            Console::stderr($message . "\n");
        } else {
            echo $message . "\n";
        }
    }

    /**
     * Colorizes a message for console output.
     * @param string $message the message to colorize.
     * @param array $format the message format.
     * @return string the colorized message.
     * @see Console::ansiFormat() for details on how to specify the message format.
     */
    protected function formatMessage($message, $format = [Console::FG_RED, Console::BOLD])
    {
        $stream = (PHP_SAPI === 'cli') ? \STDERR : \STDOUT;
        // try controller first to allow check for --color switch
        if (Yii::$app->controller instanceof \yii\console\Controller && Yii::$app->controller->isColorEnabled($stream)
            || Yii::$app instanceof \yii\console\Application && Console::streamSupportsAnsiColors($stream)) {
            $message = Console::ansiFormat($message, $format);
        }
        return $message;
    }
}






参考资料

Yii 2.0 权威指南 请求处理(Handling Requests): 错误处理(Handling Errors) https://www.yiichina.com/doc/guide/2.0/runtime-handling-errors

PHP 异常及错误处理相关 https://ibaiyang.github.io/blog/php/2019/01/23/PHP-%E5%BC%82%E5%B8%B8%E5%8F%8A%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86%E7%9B%B8%E5%85%B3.html


返回