yii2启动流程分析

2021-03-21  本文已影响0人  gurlan

前言

本文主要分析Yii2应用的启动、运行的过程,主要包括以下三部分:入口脚本、启动应用、运行应用。

入口脚本

<?php

// 定义全局变量
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');

// composer自动加载代码机制,可参考 https://segmentfault.com/a/1190000010788354
require __DIR__ . '/../vendor/autoload.php';

// 1.引入工具类Yii
// 2.注册自动加载函数
// 3.生成依赖注入中使用到的容器
require __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';

// 加载应用配置文件
$config = require __DIR__ . '/../config/web.php';

//生成应用实例并运行
(new yii\web\Application($config))->run();

启动应用

1.分析:new yii\web\Application($config)
主要就是执行父类构造函数(代码位置:vendor\yiisoft\yii2\base\Application.php)

public function __construct($config = [])
    {
          // 将Yii::$app 保存为当前对象
        Yii::$app = $this;
            
            // 见分析2
        static::setInstance($this);

          // 设置当前应用状态
        $this->state = self::STATE_BEGIN;

        // 进行一些预处理(根据$config配置应用)
        // 详细代码位置:yii\base\Application::preInit(&$config) 见分析3
        $this->preInit($config);
                
            //注册ErrorHandler,这样一旦抛出了异常或错误,就会由其负责处理
              //代码位置:yii\base\Application::registerErrorHandler(&$config) 见分析4 
        $this->registerErrorHandler($config);
    
            // 根据$config配置Application
            // 然后执行yii\web\Application::init()(实际上是执行yii\base\Application::init())见分析5
        Component::__construct($config);
    }

2.分析:yii\base\Module::setInstance($instance)

    /**
     * Sets the currently requested instance of this module class.
     * @param Module|null $instance the currently requested instance of this module class.
     * If it is `null`, the instance of the calling class will be removed, if any.
     */
    public static function setInstance($instance)
    {
        if ($instance === null) {
            unset(Yii::$app->loadedModules[get_called_class()]);
        } else {
            // 将Application实例本身记录为“已加载模块”
            Yii::$app->loadedModules[get_class($instance)] = $instance;
        }
    }

3.分析:yii\base\Application::preInit(&$config)

        /**
     * Pre-initializes the application.
     * This method is called at the beginning of the application constructor.
     * It initializes several important application properties.
     * If you override this method, please make sure you call the parent implementation.
     * @param array $config the application configuration
     * @throws InvalidConfigException if either [[id]] or [[basePath]] configuration is missing.
     */
    public function preInit(&$config)
    {
        // 1.判断$config中是否有配置项‘id’,‘basePath’(必须有,否则抛异常)
        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.');
        }
                //  2.设置别名:@vendor,@runtime
        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();
        }
                
        // 3.设置时间区域(timeZone)
        if (isset($config['timeZone'])) {
            $this->setTimeZone($config['timeZone']);
            unset($config['timeZone']);
        } elseif (!ini_get('date.timezone')) {
            $this->setTimeZone('UTC');
        }
                
        // 4.自定义配置容器(Yii::$container)的属性(由这里我们知道可以自定义配置容器)
        if (isset($config['container'])) {
            $this->setContainer($config['container']);
            unset($config['container']);
        }
                
        // 5.合并核心组件配置到自定义组件配置:数组$config['components']
        //(核心组件有哪些参考:yii\web\Application::coreComponents())
        //(注意:这个方法中$config使用了引用,所以合并$config['components']可以改变$config原来的值)
        // 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'];
            }
        }
    }

4.分析:yii\base\Application::registerErrorHandler(&$config)

    /**
     * Registers the errorHandler component as a PHP error handler.
     * @param array $config application 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);
            }
            $this->set('errorHandler', $config['components']['errorHandler']);
            unset($config['components']['errorHandler']);
            $this->getErrorHandler()->register();
        }
    }
    

    // 默认config文件关于error handler的配置,这里的配置会合并到Yii2本身对核心组件的定义中去(),核心组件的定义在yii\web\Application::coreComponents()
    'components' => [
            'errorHandler' => [
                'errorAction' => 'site/error',
            ],
    ],

        // yii\base\ErrorHandler::register()

        /**
     * Register this error handler.
     * @since 2.0.32 this will not do anything if the error handler was already registered
     */
    public function register()
    {
        if (!$this->_registered) {
            // 该选项设置是否将错误信息作为输出的一部分显示到屏幕,
                  // 或者对用户隐藏而不显示。
            ini_set('display_errors', false);
            // 当抛出异常且没有被catch的时候,由yii\base\ErrorHandler::handleException()处理
            set_exception_handler([$this, 'handleException']);
            if (defined('HHVM_VERSION')) {
                set_error_handler([$this, 'handleHhvmError']);
            } else {
                // 当出现error的时候由yii\base\ErrorHandler::handleError()处理
                set_error_handler([$this, 'handleError']);
            }
            if ($this->memoryReserveSize > 0) {
                $this->_memoryReserve = str_repeat('x', $this->memoryReserveSize);
            }
            // 当出现fatal error的时候由yii\base\ErrorHandler::handleFatalError()处理
            register_shutdown_function([$this, 'handleFatalError']);
            $this->_registered = true;
        }
    }
一个值得注意的地方:
在handleException()、handleError()、handleFatalError()中会直接或间接调用yii\web\ErrorHandle::renderException(),而在这个函数里面,有以下这一行代码
$result = Yii::$app->runAction($this->errorAction);
回顾上面config文件中对errorAction这个属性的定义,我们知道可以通过自定义一个action用于在异常或错误时显示自定义的出错页面
例如yii2中默认使用’site/error’这个action来处理

5.分析:Component::__construct($config)
(代码实际位置:/vendor/yiisoft/yii2/base/BaseObject.php)

        /**
     * Constructor.
     *
     * The default implementation does two things:
     *
     * - Initializes the object with the given configuration `$config`.
     * - Call [[init()]].
     *
     * If this method is overridden in a child class, it is recommended that
     *
     * - the last parameter of the constructor is a configuration array, like `$config` here.
     * - call the parent implementation at the end of the constructor.
     *
     * @param array $config name-value pairs that will be used to initialize the object properties
     */
    public function __construct($config = [])
    {
        if (!empty($config)) {
            // 见下面的详细分析
            Yii::configure($this, $config);
        }
        // 跳转到yii\base\Application::init(),见下面的分析
        $this->init();
    }

        
         /**
     * Configures an object with the initial property values.
     * @param object $object the object to be configured
     * @param array $properties the property initial values given in terms of name-value pairs.
     * @return object the object itself
     */
        //Yii::configure($object, $properties)分析
    //实际上就是为Application的属性赋值
    //注意:这里会触发一些魔术方法,间接调用了:
    //yii\web\Application::setHomeUrl()
    //yii\di\ServiceLocator::setComponents()(这个值得注意,详细参考本文后续“5.1分析”)
    //yii\base\Module::setModules()
    public static function configure($object, $properties)
    {
        foreach ($properties as $name => $value) {
            $object->$name = $value;
        }

        return $object;
    }
        
    /**
     * {@inheritdoc}
     */
        // yii\base\Application::init()分析
    public function init()
    {
        // 设置当前应用状态
        $this->state = self::STATE_INIT;
        // 见下面的bootstrap()方法
        $this->bootstrap();
    }
    
         /**
     * Initializes extensions and executes bootstrap components.
     * This method is called by [[init()]] after the application has been fully configured.
     * If you override this method, make sure you also call the parent implementation.
     */
    protected function bootstrap()
    {
        if ($this->extensions === null) {
           // @vendor/yiisoft/extensions.php是一些关于第三方扩展的配置,当用composer require安装第三扩展的时候就会将新的扩展的相关信息记录到该文件,这样我们就可以在代码中调用了
            $file = Yii::getAlias('@vendor/yiisoft/extensions.php');
            $this->extensions = is_file($file) ? include $file : [];
        }
        foreach ($this->extensions as $extension) {
            if (!empty($extension['alias'])) {
                foreach ($extension['alias'] as $name => $path) {
                    Yii::setAlias($name, $path);
                }
            }
            // 进行一些必要的预启动
            if (isset($extension['bootstrap'])) {
                $component = Yii::createObject($extension['bootstrap']);
                if ($component instanceof BootstrapInterface) {
                    Yii::debug('Bootstrap with ' . get_class($component) . '::bootstrap()', __METHOD__);
                    $component->bootstrap($this);
                } else {
                    Yii::debug('Bootstrap with ' . get_class($component), __METHOD__);
                }
            }
        }
                // 预启动,通常会包括‘log’组件,‘debug’模块和‘gii’模块(参考配置文件)
        foreach ($this->bootstrap as $mixed) {
            $component = null;
            if ($mixed instanceof \Closure) {
                Yii::debug('Bootstrap with Closure', __METHOD__);
                if (!$component = call_user_func($mixed, $this)) {
                    continue;
                }
            } elseif (is_string($mixed)) {
                if ($this->has($mixed)) {
                    $component = $this->get($mixed);
                } elseif ($this->hasModule($mixed)) {
                    $component = $this->getModule($mixed);
                } elseif (strpos($mixed, '\\') === false) {
                    throw new InvalidConfigException("Unknown bootstrapping component ID: $mixed");
                }
            }

            if (!isset($component)) {
                $component = Yii::createObject($mixed);
            }

            if ($component instanceof BootstrapInterface) {
                Yii::debug('Bootstrap with ' . get_class($component) . '::bootstrap()', __METHOD__);
                $component->bootstrap($this);
            } else {
                Yii::debug('Bootstrap with ' . get_class($component), __METHOD__);
            }
        }
    }

在完成了上面的流程之后,应用就算启动成功了,可以开始运行,处理请求了。

运行应用

1.分析:yii\base\Application::run()
在上面的构造函数执行完后,开始运行应用。即下面这行代码的run()部分
(new yii\web\Application($config))->run();//(实际上执行的是yii\base\Application::run())

 /**
     * Runs the application.
     * This is the main entrance of an application.
     * @return int the exit status (0 means normal, non-zero values mean abnormal)
     */
    public function run()
    {
        try {
            $this->state = self::STATE_BEFORE_REQUEST;
            // 触发事件’beforeRequest’,依次执行该事件的handler,
            $this->trigger(self::EVENT_BEFORE_REQUEST);

            $this->state = self::STATE_HANDLING_REQUEST;
            //处理请求,这里的返回值是yii\web\Response实例
            //handleRequest(),详细参考本文后续“分析2”
            $response = $this->handleRequest($this->getRequest());

            $this->state = self::STATE_AFTER_REQUEST;
            // 触发事件’afterRequest’,依次执行该事件的handler
            $this->trigger(self::EVENT_AFTER_REQUEST);
        
            $this->state = self::STATE_SENDING_RESPONSE;
            // 发送响应,详细参考本文后续“分析3”
            $response->send();

            $this->state = self::STATE_END;

            return $response->exitStatus;
        } catch (ExitException $e) {
            // 结束运行
            $this->end($e->statusCode, isset($response) ? $response : null);
            return $e->statusCode;
        }
    }

2.分析: yii\web\Application::handleRequest()

 /**
     * Handles the specified request.
     * @param Request $request the request to be handled
     * @return Response the resulting response
     * @throws NotFoundHttpException if the requested route is invalid
     */
    public function handleRequest($request)
    {
        if (empty($this->catchAll)) {
            try {
                // 从请求中解析路由和参数,这里会调用urlManager组件来处理
                list($route, $params) = $request->resolve();
            } catch (UrlNormalizerRedirectException $e) {
                $url = $e->url;
                if (is_array($url)) {
                    if (isset($url[0])) {
                        // ensure the route is absolute
                        $url[0] = '/' . ltrim($url[0], '/');
                    }
                    $url += $request->getQueryParams();
                }
                                 // 当解析请求出现异常时进行重定向
                return $this->getResponse()->redirect(Url::to($url, $e->scheme), $e->statusCode);
            }
        } else {
            // ’catchAll’参数可以在配置文件中自定义,可用于在项目需要临时下线维护时给出一个统一的访问路由
            $route = $this->catchAll[0];
            $params = $this->catchAll;
            unset($params[0]);
        }
        try {
            Yii::debug("Route requested: '$route'", __METHOD__);
            // 记录下当前请求的route
            $this->requestedRoute = $route;
            // 执行路由相对应的action(yii\base\Module::runAction()详细参考本文后续“分析3”)
            $result = $this->runAction($route, $params);
            if ($result instanceof Response) {
                return $result;
            }
                        // 如果action的返回结果不是Response的实例,则将结果封装到Response实例的data属性中
            $response = $this->getResponse();
            if ($result !== null) {
                $response->data = $result;
            }

            return $response;
        } catch (InvalidRouteException $e) {
            throw new NotFoundHttpException(Yii::t('yii', 'Page not found.'), $e->getCode(), $e);
        }
    }

3.分析: yii\base\Module::runAction()

public function runAction($route, $params = [])
{
    //根据路由创建Controller实例(详细参考本文后续“分析4”)
    $parts = $this->createController($route);
    if (is_array($parts)) {
        /* @var $controller Controller */
        // 解析出控制器和方法
        list($controller, $actionID) = $parts;
        $oldController = Yii::$app->controller;
        // 设置当前的Controller实例
        Yii::$app->controller = $controller;
        // 执行action(yii\base\Controller::runAction()详细参考本文后续“分析5”)
        $result = $controller->runAction($actionID, $params);
        if ($oldController !== null) {
            //可以看做是栈
            Yii::$app->controller = $oldController;
        }

        return $result;
    }

    $id = $this->getUniqueId();
    throw new InvalidRouteException('Unable to resolve the request "' . ($id === '' ? $route : $id . '/' . $route) . '".');
}

4.分析: yii\base\Module::createController()

         /**
     * Creates a controller instance based on the given route.
     *
     * The route should be relative to this module. The method implements the following algorithm
     * to resolve the given route:
     *
     * 1. If the route is empty, use [[defaultRoute]];
     * 2. If the first segment of the route is found in [[controllerMap]], create a controller
     *    based on the corresponding configuration found in [[controllerMap]];
     * 3. If the first segment of the route is a valid module ID as declared in [[modules]],
     *    call the module's `createController()` with the rest part of the route;
     * 4. The given route is in the format of `abc/def/xyz`. Try either `abc\DefController`
     *    or `abc\def\XyzController` class within the [[controllerNamespace|controller namespace]].
     *
     * If any of the above steps resolves into a controller, it is returned together with the rest
     * part of the route which will be treated as the action ID. Otherwise, `false` will be returned.
     *
     * @param string $route the route consisting of module, controller and action IDs.
     * @return array|bool If the controller is created successfully, it will be returned together
     * with the requested action ID. Otherwise `false` will be returned.
     * @throws InvalidConfigException if the controller class and its file do not match.
     */
    public function createController($route)
    {
         // 如果route为空则设置route为默认路由,这个可以在配置文件中自定义
        if ($route === '') {
            $route = $this->defaultRoute;
        }

        // double slashes or leading/ending slashes may cause substr problem
        $route = trim($route, '/');
        if (strpos($route, '//') !== false) {
            return false;
        }

        if (strpos($route, '/') !== false) {
            list($id, $route) = explode('/', $route, 2);
        } else {
            $id = $route;
            $route = '';
        }
            
        // 优先使用controllerMap,controllerMap可以如下
        // module and controller map take precedence
        if (isset($this->controllerMap[$id])) {
            $controller = Yii::createObject($this->controllerMap[$id], [$id, $this]);
            return [$controller, $route];
        }
        // 先判断是否存在相应的模块
        $module = $this->getModule($id);
        if ($module !== null) {
            // 当存在模块时,进行递归
            return $module->createController($route);
        }

        if (($pos = strrpos($route, '/')) !== false) {
            $id .= '/' . substr($route, 0, $pos);
            $route = substr($route, $pos + 1);
        }
                // 最终找到Controller的id
        $controller = $this->createControllerByID($id);
        if ($controller === null && $route !== '') {
            // 详细见下面的代码分析
            $controller = $this->createControllerByID($id . '/' . $route);
            $route = '';
        }
                 // 返回Controller实例和剩下的路由信息
        return $controller === null ? false : [$controller, $route];
    }
        
        /**
     * Creates a controller based on the given controller ID.
     *
     * The controller ID is relative to this module. The controller class
     * should be namespaced under [[controllerNamespace]].
     *
     * Note that this method does not check [[modules]] or [[controllerMap]].
     *
     * @param string $id the controller ID.
     * @return Controller|null the newly created controller instance, or `null` if the controller ID is invalid.
     * @throws InvalidConfigException if the controller class and its file name do not match.
     * This exception is only thrown when in debug mode.
     */
    public function createControllerByID($id)
    {
        $pos = strrpos($id, '/');
        if ($pos === false) {
            $prefix = '';
            $className = $id;
        } else {
            $prefix = substr($id, 0, $pos + 1);
            $className = substr($id, $pos + 1);
        }

        if ($this->isIncorrectClassNameOrPrefix($className, $prefix)) {
            return null;
        }

        $className = preg_replace_callback('%-([a-z0-9_])%i', function ($matches) {
                return ucfirst($matches[1]);
            }, ucfirst($className)) . 'Controller';
        $className = ltrim($this->controllerNamespace . '\\' . str_replace('/', '\\', $prefix) . $className, '\\');
        if (strpos($className, '-') !== false || !class_exists($className)) {
            return null;
        }

        if (is_subclass_of($className, 'yii\base\Controller')) {
            // 通过依赖注入容器获得Controller实例
            $controller = Yii::createObject($className, [$id, $this]);
            return get_class($controller) === $className ? $controller : null;
        } elseif (YII_DEBUG) {
            throw new InvalidConfigException('Controller class must extend from \\yii\\base\\Controller.');
        }

        return null;
    }

5.分析:yii\base\Controller::runAction()

/**
     * Runs an action within this controller with the specified action ID and parameters.
     * If the action ID is empty, the method will use [[defaultAction]].
     * @param string $id the ID of the action to be executed.
     * @param array $params the parameters (name-value pairs) to be passed to the action.
     * @return mixed the result of the action.
     * @throws InvalidRouteException if the requested action ID cannot be resolved into an action successfully.
     * @see createAction()
     */
    public function runAction($id, $params = [])
    {
        // 创建action实例,详细见下面的代码
        $action = $this->createAction($id);
        if ($action === null) {
            throw new InvalidRouteException('Unable to resolve the request: ' . $this->getUniqueId() . '/' . $id);
        }

        Yii::debug('Route to run: ' . $action->getUniqueId(), __METHOD__);

        if (Yii::$app->requestedAction === null) {
            Yii::$app->requestedAction = $action;
        }

        $oldAction = $this->action;
        $this->action = $action;

        $modules = [];
        $runAction = true;

        // 返回的modules包括该controller当前所在的module,以及该module的所有祖先module(递归直至没有祖先module)
            // 然后从最初的祖先module开始,依次执行“模块级”的beforeAction()
            // 如果有beforeAction()没有返回true, 那么会中断后续的执行
        // call beforeAction on modules
        foreach ($this->getModules() as $module) {
            if ($module->beforeAction($action)) {
                array_unshift($modules, $module);
            } else {
                $runAction = false;
                break;
            }
        }

        $result = null;
                // 执行当前控制器的beforeAction,通过后再最终执行action
          //(如果前面“模块级beforeAction”没有全部返回true,则这里不会执行)
        if ($runAction && $this->beforeAction($action)) {
            // run the action
            //代码位置:yii\base\InlineAction::runWithParams()详细参考本文后续
            $result = $action->runWithParams($params);
                        // 执行当前Controller的afterAction
            $result = $this->afterAction($action, $result);

            // call afterAction on modules
            // 从当前模块开始,执行afterAction,直至所有祖先的afterAction
            foreach ($modules as $module) {
                /* @var $module Module */
                $result = $module->afterAction($action, $result);
            }
        }

        if ($oldAction !== null) {
            $this->action = $oldAction;
        }
                // 如果有beforeAction没有通过,那么会返回null
        return $result;
    }
        
        /**
     * Creates an action based on the given action ID.
     * The method first checks if the action ID has been declared in [[actions()]]. If so,
     * it will use the configuration declared there to create the action object.
     * If not, it will look for a controller method whose name is in the format of `actionXyz`
     * where `xyz` is the action ID. If found, an [[InlineAction]] representing that
     * method will be created and returned.
     * @param string $id the action ID.
     * @return Action|null the newly created action instance. Null if the ID doesn't resolve into any action.
     */
        // 根据给定的动作ID创建一个动作。该方法首先检查动作ID是否已在[[actions()]]中声明。如果是这样,它将使用在那里声明的配置来创建操作对象。如果不是,它将寻找名称为actionXyz格式的控制器方法,其中xyz是动作ID。如果找到,将创建并返回表示该方法的[[InlineAction]]。
    public function createAction($id)
    {
        if ($id === '') {
            $id = $this->defaultAction;
        }

        $actionMap = $this->actions();
        if (isset($actionMap[$id])) {
            return Yii::createObject($actionMap[$id], [$id, $this]);
        }

        if (preg_match('/^(?:[a-z0-9_]+-)*[a-z0-9_]+$/', $id)) {
            $methodName = 'action' . str_replace(' ', '', ucwords(str_replace('-', ' ', $id)));
            if (method_exists($this, $methodName)) {
                $method = new \ReflectionMethod($this, $methodName);
                if ($method->isPublic() && $method->getName() === $methodName) {
                    // InlineAction封装了将要执行的action的相关信息,该类继承自yii\base\Action
                    return new InlineAction($id, $this, $methodName);
                }
            }
        }

        return null;
    }


6.分析: yii\web\Response::send()

         /**
     * Sends the response to the client.
     */
    public function send()
    {
        if ($this->isSent) {
            return;
        }
        // 触发事件
        $this->trigger(self::EVENT_BEFORE_SEND);
        // 预处理 
        $this->prepare();
        // 触发事件
        $this->trigger(self::EVENT_AFTER_PREPARE);
        // 发送http响应的头部,详见下面的代码
        $this->sendHeaders();
        // 发送http响应的主体,详见下面的代码
        $this->sendContent();
        // 触发事件
        $this->trigger(self::EVENT_AFTER_SEND);
        $this->isSent = true;
    }

请求生命周期

请求生命周期
  1. 用户向入口脚本 web/index.php 发起请求。
  2. 入口脚本加载应用配置并创建一个应用 实例去处理请求。
  3. 应用通过请求组件解析请求的 路由
  4. 应用创建一个控制器实例去处理请求。
  5. 控制器创建一个动作实例并针对操作执行过滤器。
  6. 如果任何一个过滤器返回失败,则动作取消。
  7. 如果所有过滤器都通过,动作将被执行。
  8. 动作会加载一个数据模型,或许是来自数据库。
  9. 动作会渲染一个视图,把数据模型提供给它。
  10. 渲染结果返回给响应组件。
  11. 响应组件发送渲染结果给用户浏览器。

静态结构

应用静态结构

每个应用都有一个入口脚本 web/index.php,这是整个应用中唯一可以访问的 PHP 脚本。 入口脚本接受一个 Web 请求并创建应用实例去处理它。 应用在它的组件辅助下解析请求, 并分派请求至 MVC 元素。视图使用小部件 去创建复杂和动态的用户界面。

上一篇下一篇

猜你喜欢

热点阅读