正文
errorHandler组件调用Yii::error()
,
Yii::error()
调用yii\log\Logger
的log()
方法追加日志,
yii\log\Logger
的flush()
方法调用yii\log\Dispatcher
的dispatch()
方法分发日志,
yii\log\Dispatcher
的dispatch()
方法调用 target 的collect()
方法收集日志,
target 的collect()
方法再调用自己的export()
方法把日志持久化到储存中。
可以看到里面有三个角色:
- 错误异常处理组件;
- 日志组件,包括日志分发操作;
- 日志持久化组件;
异常及错误处理程序注册
在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 . ' – ' . $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