前面我们介绍了那么多与应用生命周期相关联的信息,包括应用的预初始化,初始化等等,其实这些都是为运行应用而做的准备。
今天我们要介绍的内容,其实很重要,我们要讲一下这些准备工作就绪之后,应用到底是如何运行的。
应用的运行,可能要分为几个步骤,我们先从 yii\base\Application::run 方法说起。
yii\base\Application::run 方法的部分代码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15$this->state = self::STATE_BEFORE_REQUEST;
$this->trigger(self::EVENT_BEFORE_REQUEST);
$this->state = self::STATE_HANDLING_REQUEST;
$response = $this->handleRequest($this->getRequest());
$this->state = self::STATE_AFTER_REQUEST;
$this->trigger(self::EVENT_AFTER_REQUEST);
$this->state = self::STATE_SENDING_RESPONSE;
$response->send();
$this->state = self::STATE_END;
return $response->exitStatus;
代码虽然不多,但是整个流程全部看下来,还真够我们在喝一壶的。
首先,标记当前请求的生命周期状态为 yii\base\Application::STATE_BEFORE_REQUEST,即请求之前的一种状态,随后触发 yii\base\Application::EVENT_BEFORE_REQUEST 事件。
有同学可能这里就去追呀找呀,这个事件处理函数在哪呢?啥时候注册的?找了半天把自己也整迷糊了,也没看下去的心情了。这一点其实我们在yii2实战中介绍预定义事件的时候就详细的介绍了,预定义事件中的绑定事件处理函数是官方预留给我们自己处理的,这里的触发只是相当于一个预留给各位的接口,以便不时之需。
后面的分析中我们还会遇到很多此类预定义事件的触发,如无特别说明,我们就不一一描述了。不懂的可以回去先把预定义事件的几个例子再练习一下。
有些人总觉得源码里面预留的事件非常多余,平时自己也很少利用。其实很有用,我们在分析yii2源码的过程中,有兴趣的可以做个总结。
继续看run方法,触发 yii\base\Application::EVENT_BEFORE_REQUEST 事件之后,这里又标记了当前请求的生命周期状态为 yii\base\Application::STATE_HANDLING_REQUEST,即处理请求阶段。
这里并没有触发事件的操作,而是调用了 yii\web\Application::handleRequest 方法去处理请求。可想而知,捕获路由,处理路由以及根据路由规则调用对应的方法,自然就是在这个方法内完成的。
找到 yii\web\Application::handleRequest 方法,我们把该方法的实现分为以下三个部分,如下截图标识
首先,handleRequest 方法有一个参数,$request,指的是 yii\web\request 组件,我们知道,这个是内置的核心组件,可以在 yii\web\Application::coreComponents 方法中看到内置配置。
对照上图,我们接着看 handleRequest 方法中标识的三部分
①部分主要是解析路由
②部分是运行路由指定的控制器操作
执行完②部分,可想而知,③部分便是响应客户端请求了
后面我们将会就这三部分,对请求、控制器以及响应进行分析。
今天我们的重点放在第二部分,看看Controller是如何处理Action的。也就是说我们假设应用已经对路由进行了解析,合法且合理,我们今天的重点就是 controller/action 是如何执行的。
以路由 index.php?r=site/index 为例
我们先抛出10个为什么,各位有兴趣可以试着看一下源码并作相应的解答。
如何把路由解析到指定的 frontend\controller\SiteController::actionIndex 方法的?
如果有一个site模块,是会访问 siteModule/indexController/indexAction 吗?
controller类是如何创建的?action呢?
如果SiteController不是 yii\web\Controller 类的子类会如何?
如果SiteController::actions方法同样有index操作,哪个作会被执行还是都会执行?
如果SiteController::actionIndex不是public类型的呢?
beforeAction是如何被执行的?afterAction呢?
siteController::actionIndex 到底是如何被调用的?
如果actionIndex有参数,应用是如何知道我们缺少参数并抛出异常的?
actionIndex内写不写return,有什么区别吗?
我们的切入口就是上图中的第二部分的代码段,其实就是下面这一句代码引出的系列问题1
$result = $this->runAction($route, $params);
这里的$this指的自然是yii\web\Application的实例 Yii::$app,runAction方法,指的是1
yii\base\Module::runAction。
记得我们前面说过,Yii::$app即 yii\web\Application的实例,也是一个独立的module。yii\base\Module::runAction实质则是通过指定的路由运行Controller的过程。
后面的过程可能会很复杂,有兴趣的还是画画图记一下的比较好。
yii\base\Module::runAction方法有两个重点,我们标记一下1
2
3
4
5
6
7
8
9
10
11
12public function runAction($route, $params = [])
{
$parts = $this->createController($route); // ①
if (is_array($parts)) {
// ......
$result = $controller->runAction($actionID, $params); // ②
// ......
}
// ......
}
①:创建controller
②:运行controller的action
以 index.php?r=site/index 为例,yii\base\Module::createController的参数 $route 就是 site/index,为什么是它可以先忽略,毕竟路由的解析问题我们这一节点不说。
看 yii\base\Module::ccreateController方法,我们注意到1
2
3if ($route === '') {
$route = $this->defaultRoute;
}
因为在 yii\web\Application中有配置属性defaultRoute=site,所以当我们访问省略路由的时候,默认就是site了,这一点大家可以在配置文件中进行配置。
接着看源码,我们发现一段有意思的代码1
2
3
4if (isset($this->controllerMap[$id])) {
$controller = Yii::createObject($this->controllerMap[$id], [$id, $this]);
return [$controller, $route];
}
这里直接就return了,怎么回事呢?原理这里有一个controllerMap优先的原则。我们解释一下:
有时候,我们对外开放的路由地址为了体现完美,比如利于seo啦,显得高大上啦,但是稀奇古怪的英文自己又看不懂,时间一长,什么含义也不记得了,怎么办?
这里有一个controllerMap属性,假设对外的实际路由是site/index,正常情况下,下面我们要介绍的内容,他会被解析到SiteController/indexAction,但是通过controllerMap映射,我们可以把该地址映射到 sourceSite/indexAction (另一个controller对应的action)上,这一点也可以在配置文件中指定。
但是大多数情况,我们并没有这么无聊。
所以,继续看源码。
正如我们抛出的第二个问题,你有没有想过,为什么你在地址栏输入 index.php?r=gii,这个路由就被解析到gii模块了,而不是giiController?
其实就是下面这几行代码阻止了程序继续执行1
2
3
4$module = $this->getModule($id);
if ($module !== null) {
return $module->createController($route);
}
不过向gii这种的module,它既然也是一个独立的module,所以势必最终还是会重新走 yii\base\Module::createController方法,即我们还是可以通过controllerMap映射阻断他的这种解析。
好了,我们说重点。
假设我们的代码被顺利执行,现在应该要执行 yii\base\Module::createControllerID了1
$controller = $this->createControllerByID($id);
这里的id指的就是ControllerID即site了。
yii\base\Module::createControllerID有很多的过滤条件,各位可以找到该方法看一下。
很多刚开始学yii2的同学比较纳闷为啥我的路由ID是site,但是我们类文件不是class Site而是class SiteController,答案就在这个方法内。1
2$className = str_replace(' ', '', ucwords(str_replace('-', ' ', $className))) . 'Controller';
$className = ltrim($this->controllerNamespace . '\\' . str_replace('/', '\\', $prefix) . $className, '\\');
可见,这里是官方的一个规范,还就得这么写。
最终 $className 是有路由ID被格式化带有命名空间的类名,这里自然指的就是 frontend\controllers\SiteController 了。
虽然都很基础,但是还有一个问题值得各位注意,这里的controller还都得是yii\base\Controller的子类,这不这里有严谨的判断,不然它就抛出异常给你看。1
2
3
4
5if (is_subclass_of($className, 'yii\base\Controller')) {
// ......
} elseif (YII_DEBUG) {
throw ...;
}
最终,yii\base\Module::createController方法会返回yii\base\Controller的实例和操作ID。
到此,yii\base\Module::runAction的两部分我们说了第一部分,创建controller,有几个要点各位自己回味一下。
我们接着看第二部分,运行controller的action操作。
让我们跳出 yii\base\Module 的范围,来看看 SiteController。
首先我们看一下controller的几个父类。
SiteController => yii\web\Controller => yii\base\Controller => yii\base\Component,后面的父类我们就此打住。
yii\base\Module::runAction内,$controller->runAction($actionID, $params); 调用的runAction实际指的是 yii\base\Controller::runAction。
yii\base\Controller很实在,上来就先createAction,注意 createAction的参数 $id 指的是实际的action1
$action = $this->createAction($id);
我们就先看看yii\base\Controller::createAction是如何创建action的,action其实大有来头,它并不是一个简单的action。
同样,我们看到这里有判断actionID,如果是空,则使用默认的action,默认的action在yii\base\Controller::defaultAction属性上有配置。
接着我们看到有一个action的映射关系,很神奇,跟controller有点像。1
2
3
4
5
6$actionMap = $this->actions();
if (isset($actionMap[$id])) {
return Yii::createObject($actionMap[$id], [$id, $this]);
} elseif (preg_match('/^[a-z0-9\\-_]+$/', $id) && strpos($id, '--') === false && trim($id, '-') === $id) {
// ......
}
所以说我们我们的action又在actions方法中配置,则执行actions方法配置的优先策略。
否则,校验actionID是否合法,合法的情况下,比如siteController的index操作,则认为 actionIndex就是我们要找的方法。需要注意的是,这里的前缀“action”同样是源码内的规定,不这么写就是不行,没得商量。
接着我们看到,还有更严谨的判断,通过反射,会严格要求此方法是public可访问的,最终返回的结果是 yii\base\InlineAction 的实例。1
2
3
4$method = new \ReflectionMethod($this, $methodName);
if ($method->isPublic() && $method->getName() === $methodName) {
return new InlineAction($id, $this, $methodName);
}
我们等会再来看看这个 yii\base\InlineAction,这里先记住yii\base\Controller::createAction返回的是 yii\base\InlineAction 的实例。
有些人可能都看晕乎了,绕来绕去的,看不明白。一定要跟随我们的“足迹”,一步一步的分析,看看哪里都做了什么,整体看下来就很好理解了。
接着来看 yii\base\Controller::runAction。
我们一再强调,yii::$app就是一个独立的module,这不,这里又派上用场了。1
2
3
4
5
6
7
8
9
10$runAction = true;
foreach ($this->getModules() as $module) {
if ($module->beforeAction($action)) {
array_unshift($modules, $module);
} else {
$runAction = false;
break;
}
}
这段话是什么意思呢?简单的说,就是执行module的beforeAction,注意有一点,如果beforeAction返回了false,$runAction就是false,从下面我们可以看得出来,如果 $runAction是false,yii\base\Controller最终的返回结果就是null,继而,yii\base\Module::runAction也返回null,最终我们返回给yii\web\Application::handleRequest null,这显然不是我们想要的,具体我们后面在做分析。
所以,很多时候,很多同学写beforeAction,都没搞明白一个问题,由于少写了一句 return true,页面竟然空白了,调试一整天,最后好了还是不明白为啥,反正就这么好了。
我们回到对 module::beforeAction 的调用,先看代码吧1
2
3
4
5
6
7
8foreach ($this->getModules() as $module) {
if ($module->beforeAction($action)) {
array_unshift($modules, $module);
} else {
$runAction = false;
break;
}
}
由于这里的 $module 指的是 yii\web\Application,所以 yii\web\Application::beforeAction 先被调起,其含义指的是先发起对应用级别的 beforeAction 事件调用。yii\web\Application::beforeAction 这里指的是 yii\base\Module::beforeAction。
随后,确认 $runAction 为真(应用级别的beforeAction事件返回结果都是true)的情况下,再调用 controller 级别的 beforeAction 事件。这里指的是 yii\web\Application::beforeAction方法。
代码如下1
2
3
4
5
6
7
8
9
10
11
12if ($runAction && $this->beforeAction($action)) {
// run the action
$result = $action->runWithParams($params);
$result = $this->afterAction($action, $result);
// call afterAction on modules
foreach ($modules as $module) {
/* @var $module Module */
$result = $module->afterAction($action, $result);
}
}
在以上beforeAction都返回真的情况下,调用 $action->runWithParams,我们刚刚介绍过 $action是 yii\base\InlineAction 的实例,所以这里继而会调用 yii\base\InlineAction::runWithParams 方法执行action操作。
我们找到 yii\base\InlineAction::runWithParams 方法,看其具体含义。
我们知道,具体的action操作比如actionIndex方法,我们是可以指定参数的,一旦实际的请求缺少参数,程序就会报错。这一切的判断其实都来自 yii\base\InlineAction::runWithParams 内的程序代码。
首先我们看到,这里调用了 yii\web\Controller::bindActionParams方法为action绑定参数1
$args = $this->controller->bindActionParams($this, $params);
没错, yii\web\Controller::bindActionParams 就专门为action处理参数问题,不用看该方法我们也能猜出来,该方法的实现绝对离不开反射的应用。没错,事实也确实如此。
最终, yii\base\InlineAction::runWithParams 会通过 call_user_func_array 函数调用 controller::action方法,这也解决了我们一开始抛出的问题8。
注意哦,如果action内,我们没有return, yii\base\InlineAction::runWithParams 最终返回的结果就是null。
过去有些同学总是以为执行完controller::action应用的生命周期就结束了,实则不然。所以我们在action内操作之后,往往都需要使用return,其作用就在于此。
return 的结果具体有什么用呢?
从yii\base\Controller::runAction中我们可以看出,yii\base\InlineAction::runWithParams的返回值,交给了yii\web\Controller::afterAction处理,这是预留的另外一个事件,方便大家使用。
之后,会继续调用 module的afterAction,处理一些应用级别的afterAction事件,有些人觉得这很没用,其实不然,大有用处,我们分析到后面你自会明白。
action执行完了之后,不知道你还有没有印象,我们是从哪里跳到这里来的。
action执行完了之后,这一切的结果,交由yii\base\Module::runAction方法处理,当然,这个方法也只是一个过程,这个方法的调用,源于最初的运行应用的方法 yii\web\Application::handleRequest 方法。
再后面,便是 response 的响应处理了,与前面的request对请求的处理,我们后面再做分析。