❥(^_-) Yii2框架源码解析之错误和异常处理

前言

所有的框架的错误处理机制,都在整个框架运行的顺序中排在前列,一般错误处理机制排在常量定义、配置加载、类的自动加载之后,排在其他流程逻辑之前。

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(),分别注册了异常处理,错误处理,程序终止处理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值