之前的文章 弄清了一次请求的开始到结束.主要讲了 Yii Applicaton 实例的创建,初始化,UrlManager 如何返回 Yii 中的路由信息,到 runAction,最后将 Response 发送给客户端.这其中略过了 runAction($route) 到底是如何找到以及调用对应的控制器中的方法的,下面继续从源码入手.
1,继承关系
首先我们弄清楚 Yii 几个重要类的继承关系:
yii\web\Application extends yii\base\Application
yii\base\Application extends yii\base\Module
yii\base\Module extends yii\di\ServiceLocator
yii\di\ServiceLocator extends yii\base\Component
yii\base\Component extends yii\base\Object
2,从 runAction 继续
找到
yii\web\Application
的 handleRequest 方法,这里对 runAction 进行了调用:
list($route, $params) = $request - >resolve();
$result = $this - >runAction($route, $params);
runAction 的定义位于父类的父类 yii\base\Module 中,这里已经获取到的信息为:通过 yii\web\Request 对当前 url 请求的解析,返回的一个路由.这个路由 $route 的格式为 site/index 这种形式的,或者为附加模块信息 Metting/attender/index 这种形式的.
//yii\base\Module
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 种情况做处理:
如果 $route 为空,那么直接使用 defaultRoute,可以看到 defaultRoute 在 yii\web\Application 中定义,默认值为 site
如果 $route 的第一部分 ($id) 匹配到了我们 config/main.php modules 中的某一项,那么会使用路由的剩余部分 ($route) 作为参数递归调用
createController($route)
如果 controllerMap 中发现了以第一部分 ($id) 为 key 的项,那么会优先按照 controllerMap 中的配置来创建 controller 实例
因为模块可以无限的嵌套下去,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 如何处理:
//yii\base\Module
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' => 'backend\modules\Metting\Module',
],
]
所以,当找到这个 module 后,就是根据我们 config 的这个信息来实例化一个 Module, 比如上面的 Metting/attender/index 的例子:
调用 $module = getModule('Metting')
返回
backend\modules\Metting\Module
的实例
然后
$module - >createController('attender/index')
这里是递归的调用,依然会检查 attender 是否是一个 module,返回 null,继续执行 createController 下面的代码:
//yii\base\Module 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 中配置的值,如:
backend\controllers
.如果是模块,$module 是我们自己写的 Module.php, 它继承了 yii\base\Module,并且声明了 controllerNamespace,如:
//backend\modules\Metting\Module.php
public $controllerNamespace = 'backend\modules\Metting\controllers';
所以可以看出 createController,模块与非模块 controller 实例的创建,目的是找到正确的命名空间下的控制器.
最终 createController 返回
[控制器实例, action名]
[疑问] 存在一个问题没有说明的是 $_modules 里面的数据是何时以及如何初始化的?见下面: 5,$_modules 初始化说明.
4,run controller action
上面我们看到,已经根据路由找到并创建了 controller 实例对象,接下里的操作就是回到 Module 中的 runAction 继续执行,调用控制器里面的 action 了
//yii\base\Module runAction
$result = $controller - >runAction($actionID, $params);
因为所有的 controller 类都继承了 yii\web\Controller,所以我们到这里找到 runAction 方法,位于其父类
yii\base\Controller
中,通过源码可以看到这个 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 的初始化痕迹.然后注意到
yii\base\Application
中的构造方法的最后还有一句:
Component: :__construct($config)
,就是又主动的调用了 Component 的构造方法,事实上 Component 自己没有实现构造方法而是继承自他的父类:yii\base\Object
public
function __construct($config = []) {
if (!empty($config)) {
Yii: :configure($this, $config);
}
$this - >init();
}
重点在这个
Yii: :configure($this, $config)
,其实 configure 的操作很简单,就是遍历 $config 数组,以 key 作为对象的成员属性,对应的 value 作为属性的值进行初始化操作,这里的 $this 此时就是
yii\web\Application
对象了.
//yii\BaseYii
public static
function configure($object, $properties) {
foreach($properties as $name = >$value) {
$object - >$name = $value;
}
return $object;
}
以上面我们讲的 config 中的 modules 为例,那就是:
$object->modules = [
'Metting' => [
'class' => 'backend\modules\Metting\Module',
],
]
但是我们找遍整个类以及继承的类,都没有找到 $modules 这个属性,那 Yii 是怎么做的呢?其实就是使用了 PHP 的魔术方法,打开 yii\base\Component,可以看到 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 函数名,方法名,类名不区分大小写, 所以找到了位于 yii\web\Module 中的:
public
function setModules($modules) {
foreach($modules as $id = >$module) {
$this - >_modules[$id] = $module;
}
}
终于,找了了 $_modules 中值得由来.
来源: https://www.cnblogs.com/skyfynn/p/8315457.html