Yii2.0源码阅读-从路由到控制器
之前的文章弄清了一次请求的开始到结束。主要讲了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中值得由来。
Yii2.0源码阅读-从路由到控制器的更多相关文章
- Yii2.0源码阅读-一次请求的完整过程
Yii2.0框架源码阅读,从请求发起,到结束的运行步骤 其实最初阅读是从yii\web\UrlManager这个类开始看起,不断的寻找这个类中方法的调用者,最终回到了yii\web\Applicati ...
- Yii2.0源码阅读-视图(View)渲染过程
之前的文章我们根据源码的分析,弄清了Yii如何处理一次请求,以及根据解析的路由如何调用控制器中的action,那接下来好奇的可能就是,我在控制器action中执行了return $this->r ...
- Yii2.0源码阅读-behavior的实现原理
Yii2.0中的一个思想就是组件化的思想,所以.大多数的类都直接或间接的继承自yii\base\Component,而组件的三大功能:属性.事件.行为. 行为的目的是为了方便的扩展一个类的功能,而不需 ...
- Yii2.0源码阅读-PHP如何与redis通信?
PHP与Redis可以通过socket进行通信,前提是PHP需要实现Redis的协议 RESP协议描述: 字符串 \r\n : 表示一个正确的状态信息,具体信息是'+'后面的字符(Simple Str ...
- Yii2.0源码分析之——控制器文件分析(Controller.php)创建动作、执行动作
在Yii中,当请求一个Url的时候,首先在application中获取request信息,然后由request通过urlManager解析出route,再在Module中根据route来创建contr ...
- Vue2.0源码阅读笔记(四):nextTick
在阅读 nextTick 的源码之前,要先弄明白 JS 执行环境运行机制,介绍 JS 执行环境的事件循环机制的文章很多,大部分都阐述的比较笼统,甚至有些文章说的是错误的,以下为个人理解,如有错误, ...
- Vue2.0源码阅读笔记--生命周期
一.Vue2.0的生命周期 Vue2.0的整个生命周期有八个:分别是 1.beforeCreate,2.created,3.beforeMount,4.mounted,5.beforeUpdate,6 ...
- Vue2.0源码阅读笔记--双向绑定实现原理
上一篇 文章 了解了Vue.js的生命周期.这篇分析Observe Data过程,了解Vue.js的双向数据绑定实现原理. 一.实现双向绑定的做法 前端MVVM最令人激动的就是双向绑定机制了,实现双向 ...
- Vue2.0源码阅读笔记(二):响应式原理
Vue是数据驱动的框架,在修改数据时,视图会进行更新.数据响应式系统使得状态管理变的简单直接,在开发过程中减少与DOM元素的接触.而深入学习其中的原理十分有必要,能够回避一些常见的问题,使开发变的 ...
随机推荐
- 分布式:2PC,3PC,Paxos,Raft,ISR [转]
本文主要讲述2PC及3PC,以及Paxos以及Raft协议. 两类一致性(操作原子性与副本一致性) 2PC协议用于保证属于多个数据分片上的操作的原子性.这些数据分片可能分布在不同的服务器上,2PC协议 ...
- IntelliJ IDEA 热部署插件 JRebel 安装激活及使用
JRebel对个人用户免费使用,但是要使用Facebook账号把信息完整填写之后才能获取Lisense,登陆Facebook官网二种方案:①修改本地hosts文件:②使用VPNFQ 以下是二种方案的解 ...
- 【bird-front】前端框架介绍
bird前端项目,基于react.antd.antd-admin,封装常用数据组件,细粒度权限解决方案. bird-front是基于react的后台管理系统前端项目,框架构建部分严重借鉴于antd管理 ...
- iOS UI特效
1.iOS特效 a.对应APP中的基本动作分三类: 1.指向性动效(滑动,弹出等) 2.提示性动效(滑动删除,下拉刷新等) 3.空间扩展(翻动,放大等) b.这类动效在设计过程中需要主意几点: 1.系 ...
- xcode 没有 iphone4s 模拟器 的解决方法..
项目需要iphone 4s 测试. 首先是Xcode 8 没有 iphone 4s的模拟器了. 由于线上安装的方式,不管是在code的add simulator 还是 ruby gem 的code-i ...
- KVO等具体实现步骤以及注意事项
KVO是一种设计模式,名为观察者. addObserver:forKeyPath:options:context: 通知其他对象的方法,这个方法在NSObject中就已经申明了,也就是说任何继承自NS ...
- Python学习日记:day7-----集合
1.基础数据类型汇总补充 1,list: 在循环一个列表是,最好不能删除列表中的元素. 2,bool 空列表.int:0.空str.空dict.空set--->bool:false 其余为tru ...
- xamarin android打开拍照
xamarin android打开摄像头 Intent intentBrowser = new Intent("android.media.action.IMAGE_CAPTURE" ...
- Less is exponentially more
Less is exponentially more (原文出处:rob pike 博客,https://commandcenter.blogspot.jp/2012/06/less-is-expo ...
- vue2.0父子组件以及非父子组件如何通信
1.父组件传递数据给子组件 父组件数据如何传递给子组件呢?可以通过props属性来实现 父组件: <parent> <child :child-msg="msg" ...