前言
所有的框架的错误处理机制,都在整个框架运行的顺序中排在前列,一般错误处理机制排在常量定义、配置加载、类的自动加载之后,排在其他流程逻辑之前。
base\Application
错误处理的入口是应用类的基类base\Application的构造方法中实现
public function __construct($config = [])
{
// 当application基类被继承之后,Yii::$app就变成了继承子类。
Yii::$app = $this;
// 将当前应用注册到$this->loadedModules
static::setInstance($this);
// 表示应用已开始
$this->state = self::STATE_BEGIN;
// 下面的配置数组传引用
// 初始化准备
$this->preInit($config);
// 注册错误处理
$this->registerErrorHandler($config);
// 组件初始化
Component::__construct($config);
}
public function preInit(&$config)
{
// 配置中id 和 basePath 必传, 表示应用的id和应用的部署根路径
if (!isset($config['id'])) {
throw new InvalidConfigException('The "id" configuration for the Application is required.');
}
if (isset($config['basePath'])) {
$this->setBasePath($config['basePath']);
unset($config['basePath']);
} else {
throw new InvalidConfigException('The "basePath" configuration for the Application is required.');
}
// 设置一些路径
if (isset($config['vendorPath'])) {
$this->setVendorPath($config['vendorPath']);
unset($config['vendorPath']);
} else {
// set "@vendor"
$this->getVendorPath();
}
if (isset($config['runtimePath'])) {
$this->setRuntimePath($config['runtimePath']);
unset($config['runtimePath']);
} else {
// set "@runtime"
$this->getRuntimePath();
}
// 设置时区
if (isset($config['timeZone'])) {
$this->setTimeZone($config['timeZone']);
unset($config['timeZone']);
} elseif (!ini_get('date.timezone')) {
$this->setTimeZone('UTC');
}
// 如果存在容器的配置,就设置容器的初始属性
if (isset($config['container'])) {
$this->setContainer($config['container']);
unset($config['container']);
}
// merge core components with custom components
// 合并核心组件的初始配置
foreach ($this->coreComponents() as $id => $component) {
if (!isset($config['components'][$id])) {
$config['components'][$id] = $component;
} elseif (is_array($config['components'][$id]) && !isset($config['components'][$id]['class'])) {
$config['components'][$id]['class'] = $component['class'];
}
}
}
我们可以看到,在错误注册之前,做了一些对$config的一些初始化的准备操作。就是将$config里面的某些配置项,赋值到当前应用的具体属性。并且,在Component没有初始化之前,$config在preInit()和registerErrorHandler()方法中是引用传值,表示$config可以在方法体中被修改。我们接下来看下错误处理的方法registerErrorHandler()
registerErrorHandler()
protected function registerErrorHandler(&$config)
{
// 注册php错误处理
// 开启了错误处理,则进行错误处理
if (YII_ENABLE_ERROR_HANDLER) {
// 配置组件中,如果没有错误处理类,那么就直接挂
// 注意,错误处理errorHandler的默认核心类在web\Application也就是子类里面,将获取核心组件配置覆盖写,并merge了。
if (!isset($config['components']['errorHandler']['class'])) {
echo "Error: no errorHandler component is configured.\n";
exit(1);
}
// 将错误处理类,注册到全局的组件树上。
$this->set('errorHandler', $config['components']['errorHandler']);
// 组件可以全局获取了。配置中没有必要存在了。避免Component初始化的时候,设置属性报错。
unset($config['components']['errorHandler']);
// 真正的错误处理机制
$this->getErrorHandler()->register();
}
}
可以看到,registerErrorHandler()方法只是将错误异常处理类加载到全局组件components上。真正执行错误处理的方法再ErrorHandler.php这个类里面。这种方式很好的实现了程序的解耦。
ErrorHandler.php
public function register()
{
// 错误和异常的注册
// 单例,避免重复执行
if (!$this->_registered) {
// 关闭错误显示,设置异常处理的方法
ini_set('display_errors', false);
set_exception_handler([$this, 'handleException']);
// 使用 HHVM_VERSION 判断是否已经定义,存在代表是当前运行环境是虚拟机环境。分别设置错误处理方法。
if (defined('HHVM_VERSION')) {
set_error_handler([$this, 'handleHhvmError']);
} else {
set_error_handler([$this, 'handleError']);
}
// 如果保留了内存(用于处理内存溢出的致命错误)
if ($this->memoryReserveSize > 0) {
// 将"x"重复写$this->memoryReserveSize次,保留起来。(即保留的内存)
$this->_memoryReserve = str_repeat('x', $this->memoryReserveSize);
}
// 程序终止,处理函数
register_shutdown_function([$this, 'handleFatalError']);
$this->_registered = true;
}
}
public function handleException($exception)
{
// 异常处理的方法。使用前得先注册。通过php的异常处理机制来实现。
// 如果应用程序正常终止,则不做处理。
if ($exception instanceof ExitException) {
return;
}
$this->exception = $exception;
// 处理异常的时候,先禁用错误的捕获,避免出现递归。
$this->unregister();
// 设置http状态码
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) {
$this->handleFallbackExceptionMessage($e, $exception);
} catch (\Throwable $e) {
$this->handleFallbackExceptionMessage($e, $exception);
}
// 异常处理完了,就置空。
$this->exception = null;
}
public function unregister()
{
// 恢复php的错误处理和异常处理。即取消了当前错误类的初始化(注册)。
if ($this->_registered) {
restore_error_handler();
restore_exception_handler();
$this->_registered = false;
}
}
public function handleError($code, $message, $file, $line)
{
if (error_reporting() & $code) {
// 错误类手动导入,避免自动加载发生错误的时候,导致错误处理机制无法正常运行。
if (!class_exists('yii\\base\\ErrorException', false)) {
require_once __DIR__ . '/ErrorException.php';
}
$exception = new ErrorException($message, $code, $code, $file, $line);
if (PHP_VERSION_ID < 70400) {
// 在 PHP 7.4 之前,不能在 __toString() 内部抛出异常 - 它会导致致命错误
$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;
}
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);
}
}
可以看到register()方法中,使用了函数:set_exception_handler()、set_error_handler()、register_shutdown_function(),分别注册了异常处理,错误处理,程序终止处理。
|