zoukankan      html  css  js  c++  java
  • Yii2.0源码阅读-从路由到控制器

    之前的文章弄清了一次请求的开始到结束。主要讲了Yii Applicaton实例的创建、初始化,UrlManager如何返回Yii中的路由信息,到runAction,最后将Response发送给客户端。这其中略过了runAction($route)到底是如何找到以及调用对应的控制器中的方法的,下面继续从源码入手。

    1、继承关系

    首先我们弄清楚Yii几个重要类的继承关系:

    • yiiwebApplication extends yiiaseApplication
    • yiiaseApplication extends yiiaseModule
    • yiiaseModule extends yiidiServiceLocator
    • yiidiServiceLocator extends yiiaseComponent
    • yiiaseComponent extends yiiaseObject

    2、从runAction继续

    找到yiiwebApplication的handleRequest方法,这里对runAction进行了调用:

    list ($route, $params) = $request->resolve();
    $result = $this->runAction($route, $params);
    

    runAction的定义位于父类的父类yiiaseModule中,这里已经获取到的信息为:通过yiiwebRequest对当前url请求的解析,返回的一个路由。这个路由$route的格式为site/index 这种形式的,或者为附加模块信息Metting/attender/index 这种形式的。

    //yiiaseModule
    public function runAction($route, $params = [])
    {
        $parts = $this->createController($route);
        if (is_array($parts)) {
            /* @var $controller Controller */
            list($controller, $actionID) = $parts;
            $oldController = Yii::$app->controller;
            Yii::$app->controller = $controller;
            $result = $controller->runAction($actionID, $params);
            Yii::$app->controller = $oldController;
    
            return $result;
        } else {
            $id = $this->getUniqueId();
            throw new InvalidRouteException('Unable to resolve the request "' . ($id === '' ? $route : $id . '/' . $route) . '".');
        }
    }
    

    可以看到,runAction的第一个操作就是根据$route创建controller实例。

    3、创建controller实例对象

    这个方法最终的目的就是创建一个控制器实例对象,要么是controllers中的一个控制器,要么是modules的controllers中的一个控制器。

    createController的注释也写的比较明白,一共针对4种情况做处理:

    1. 如果$route为空,那么直接使用defaultRoute,可以看到defaultRoute在yiiwebApplication中定义,默认值为site
    2. 如果$route的第一部分($id)匹配到了我们config/main.php modules中的某一项,那么会使用路由的剩余部分($route)作为参数递归调用createController($route)
    3. 如果controllerMap中发现了以第一部分($id)为key的项,那么会优先按照controllerMap中的配置来创建controller实例
    4. 因为模块可以无限的嵌套下去,yii2会递归的调用createController来创建实例

    首先说一下方法中$route的处理,除去两侧的斜线/,对$route中是否存在双斜线进行判断,然后就是将$route按照/分为第一部分$id和第二部分,新的$route

    public function createController($route)
    {
    	if (strpos($route, '/') !== false) {
    		//将Metting/attender/index分为 $id='Metting'; $route='attender/index'
            list ($id, $route) = explode('/', $route, 2);
        } else {
            $id = $route;
            $route = '';
        }
        //这里就是上面说的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);
        }
        //下面代码暂时省略
    }
    

    【注】controllerMap说明:有的时候我们定义的路由可能不想跟控制器名一致,比如引入了第三方的库,那里面的控制器名你没有办法改变。而controllerMap是一个可配置项,在你的配置文件中可以自定义

    如果controllerMap中不存在此id为key的项,那么这个id(比如Metting)会当做module来进行获取:$module = $this->getModule($id);看getModule如何处理:

    //yiiaseModule
    public function getModule($id, $load = true)
    {
    	//先不看子module的情况
        if (($pos = strpos($id, '/')) !== false) {
            // sub-module
            $module = $this->getModule(substr($id, 0, $pos));
    
            return $module === null ? null : $module->getModule(substr($id, $pos + 1), $load);
        }
    	//这里是$id(Metting)这个模块是否存在的判断逻辑
    	//如果在_modules中找到了这个元素并且是Module的实例,那么直接返回
    	//否则根据配置信息创建对象并返回
        if (isset($this->_modules[$id])) {
            if ($this->_modules[$id] instanceof Module) {
                return $this->_modules[$id];
            } elseif ($load) {
                Yii::trace("Loading module: $id", __METHOD__);
                /* @var $module Module */
                $module = Yii::createObject($this->_modules[$id], [$id, $this]);
                $module->setInstance($module);
                return $this->_modules[$id] = $module;
            }
        }
    
        return null;
    }
    

    这里我们对$_modules这个成员变量进行打印,可以看到结果就是我们在config/main.php中的modules这个数组中的配置:

    //config/main.php
    'modules' => [
    	'Metting' => [
             'class' => 'backendmodulesMettingModule',
         ],
    ]
    

    所以,当找到这个module后,就是根据我们config的这个信息来实例化一个Module,比如上面的Metting/attender/index的例子:

    • 调用 $module = getModule('Metting')
    • 返回backendmodulesMettingModule的实例
    • 然后$module->createController('attender/index')

    这里是递归的调用,依然会检查attender是否是一个module,返回null,继续执行createController下面的代码:

    //yiiaseModule createController($route) 代码简化
    $controller = $this->createControllerByID($id);
    if ($controller === null && $route !== '') {
        $controller = $this->createControllerByID($id . '/' . $route);
        $route = '';
    }
    return $controller === null ? false : [$controller, $route];
    

    createControllerByID的操作就比较简单了,主要:

    • 检查传过来的这个controller id是否合法,满足/^[a-z][a-z0-9\-_]*$/
    • 连字符(-)转为大写,拼接命名空间(controllerNamespace)信息,拼接'Controller'后缀
    • 调用Yii::createObject创建控制器对象

    关于controllerNamespace,如果访问的不是模块,那么使用的就是默认的命名空间,即我们在config/main.php中配置的值,如:backendcontrollers。如果是模块,$module是我们自己写的Module.php, 它继承了yiiaseModule,并且声明了controllerNamespace,如:

    //backendmodulesMettingModule.php
    public $controllerNamespace = 'backendmodulesMettingcontrollers';
    

    所以可以看出createController,模块与非模块controller实例的创建,目的是找到正确的命名空间下的控制器。

    最终createController返回[控制器实例, action名]

    【疑问】存在一个问题没有说明的是$_modules里面的数据是何时以及如何初始化的?见下面:5、$_modules初始化说明。

    4、run controller action

    上面我们看到,已经根据路由找到并创建了controller实例对象,接下里的操作就是回到Module中的runAction继续执行,调用控制器里面的action了

    //yiiaseModule runAction
    $result = $controller->runAction($actionID,$params);
    

    因为所有的controller类都继承了yiiwebController,所以我们到这里找到runAction方法,位于其父类yiiaseController中,通过源码可以看到这个runAction主要执行了:

    • 根据actionID创建一个InlineAction对象,内部是通过反射判断了这个方法是否存在,InlineActoin对象保存了当前的controller对象以及actionMethod信息
    • 执行生命周期函数$module->beforeAction($action)
    • beforeAction执行完毕调用$result = $action->runWithParams($params)
    • 执行生命周期函数$model->afterAction($action)

    $action->runWithParams($params)就比较简单了,就是使用了我们之前保存在InlineAction对象中的信息,执行call_user_func_array([$this->controller, $this->actionMethod], $args);

    至此,如何从url 到 Yii路由 到 controller 到 action的流程就分析清楚了。

    【附】路由到控制器执行流程图

    5、$_modules初始化说明

    通过之前的Application的构造方法,以及init方法只是初始化了bootstrap、extension以及core components等,丝毫没有配置文件中module的初始化痕迹。然后注意到yiiaseApplication中的构造方法的最后还有一句:Component::__construct($config),就是又主动的调用了Component的构造方法,事实上Component自己没有实现构造方法而是继承自他的父类:yiiaseObject

    public function __construct($config = [])
    {
        if (!empty($config)) {
            Yii::configure($this, $config);
        }
        $this->init();
    }
    

    重点在这个Yii::configure($this, $config),其实configure的操作很简单,就是遍历$config数组,以key作为对象的成员属性,对应的value作为属性的值进行初始化操作,这里的$this此时就是yiiwebApplication对象了。

    //yiiBaseYii
    public static function configure($object, $properties)
    {
        foreach ($properties as $name => $value) {
            $object->$name = $value;
        }
        return $object;
    }
    

    以上面我们讲的config中的modules为例,那就是:

    $object->modules = [
    	'Metting' => [
             'class' => 'backendmodulesMettingModule',
         ],
    ]
    

    但是我们找遍整个类以及继承的类,都没有找到$modules这个属性,那Yii是怎么做的呢?其实就是使用了PHP的魔术方法,打开yiiaseComponent,可以看到yii2定义了__set __get __call __isset等常用的魔术方法,在属性赋值的时候,我们知道,如果属性不存在会执行__set($name,$value).

    public function __set($name, $value)
    {
        $setter = 'set' . $name;
        if (method_exists($this, $setter)) {
            // set property
            $this->$setter($value);
            return;
        } 
        //下面的代码暂时省略
    }
    

    根据代码我们知道,要对这些属性初始化,那么就要实现相应的setter方法,对于modules来说就是setmodules,由于PHP函数名,方法名,类名不区分大小写,所以找到了位于yiiwebModule中的:

    public function setModules($modules)
    {
        foreach ($modules as $id => $module) {
            $this->_modules[$id] = $module;
        }
    }
    

    终于,找了了$_modules中值得由来。

  • 相关阅读:
    Flink 的datastreamAPI 以及function函数的调用,性能调优
    Spark Shuffle原理、Shuffle操作问题解决和参数调优
    Spark学习之JavaRdd
    Redis学习笔记--Redis数据过期策略详解==转
    Elasticsearch 数据搜索篇·【入门级干货】===转
    HBase二级索引的设计(案例讲解)
    C中指针符*和取址符&
    java 中,如何获取文件的MD5值呢?如何比较两个文件是否完全相同呢?
    Mysql数据库的加密与解密
    Lucene 分词
  • 原文地址:https://www.cnblogs.com/skyfynn/p/8315457.html
Copyright © 2011-2022 走看看