YII2源码分析(1) --- 基本流程分析
前言
本文主要分析Yii2应用的启动、运行的过程,主要包括以下三部分:入口脚本、启动应用、运行应用。
在分析源码的过程中主要借助了Xdebug工具。
入口脚本
文件位置:web\index.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();
分析: (new yii\web\Application($config))->run()
- 根据$config配置,生成应用实例(启动应用:new yii\web\Application($config))
- 运行应用实例(运行应用:yii\web\Application::run())
启动应用:new yii\web\Application($config)
分析:new yii\web\Application($config)
主要就是执行构造函数(代码位置:vendor\yiisoft\yii2\base\Application.php)
public function __construct($config = []) { //将Yii::$app指向当前的Application实例,因此后续就可以通过Yii::$app来调用应用 Yii::$app = $this; //调用yii\base\Module::setInstance($instance) //将Application实例本身记录为“已加载模块” //详细参考本文后续“1-1分析” static::setInstance($this); //设置当前应用状态 $this->state = self::STATE_BEGIN; //进行一些预处理(根据$config配置应用) //详细代码位置:yii\base\Application::preInit(&$config) //主要根据config文件做了以下这些事情 //1.判断$config中是否有配置项‘id’(必须有,否则抛异常) //2.设置别名:@app,@vendor,@bower,@npm,@runtime //3.设置时间区域(timeZone) //4.自定义配置容器(Yii::$container)的属性(由这里我们知道可以自定义配置容器) //5.合并核心组件配置到自定义组件配置:数组$config['components'] //(核心组件有哪些参考:yii\web\Application::coreComponents()) //(注意:这个方法中$config使用了引用,所以合并$config['components']可以改变$config原来的值) $this->preInit($config); //注册ErrorHandler,这样一旦抛出了异常或错误,就会由其负责处理 //代码位置:yii\base\Application::registerErrorHandler(&$config) //详细参考本文后续“1-2分析” $this->registerErrorHandler($config); //根据$config配置Application //然后执行yii\web\Application::init()(实际上是执行yii\base\Application::init()) //详细参考本文后续“1-3分析” Component::__construct($config); }
1-1分析:yii\base\Module::setInstance($instance)
public static function setInstance($instance) { if ($instance === null) { unset(Yii::$app->loadedModules[get_called_class()]); } else { //将Application实例本身记录为“已加载模块” Yii::$app->loadedModules[get_class($instance)] = $instance; } }
1-2分析:yii\base\Application::registerErrorHandler(&$config)
//yii\web\Application的 $errorHandler 对应 yii\web\ErrorHandler(由yii\web\Application::coreComponents()得知) //而yii\web\ErrorHandler继承自yii\base\ErrorHandler 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); } //可以看到就是根据$config['components']['errorHandler']来配置 $this->set('errorHandler', $config['components']['errorHandler']); unset($config['components']['errorHandler']); //通过PHP函数注册error handler(具体参考下面的yii\base\ErrorHandler::register()) $this->getErrorHandler()->register(); } } //默认config文件关于error handler的配置,这里的配置会合并到Yii2本身对核心组件的定义中去(),核心组件的定义在yii\web\Application::coreComponents() 'components' => [ 'errorHandler' => [ 'errorAction' => 'site/error', ], ], //yii\base\ErrorHandler::register() public function register() { //该选项设置是否将错误信息作为输出的一部分显示到屏幕, //或者对用户隐藏而不显示。 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']); } 一个值得注意的地方: 在handleException()、handleError()、handleFatalError()中会直接或间接调用yii\web\ErrorHandle::renderException(),而在这个函数里面,有以下这一行代码 $result = Yii::$app->runAction($this->errorAction); 回顾上面config文件中对errorAction这个属性的定义,我们知道可以通过自定义一个action用于在异常或错误时显示自定义的出错页面 例如yii2中默认使用’site/error’这个action来处理
1-3分析:Component::__construct($config)
(代码实际位置:vendor\yiisoft\yii2\base\Object.php)
public function __construct($config = []) { if (!empty($config)) { //见下面的详细分析 Yii::configure($this, $config); } //跳转到yii\base\Application::init(),见下面的分析 $this->init(); } //Yii::configure($object, $properties)分析 //实际上就是为Application的属性赋值 //注意:这里会触发一些魔术方法,间接调用了: //yii\web\Application::setHomeUrl() //yii\di\ServiceLocator::setComponents()(这个值得注意,详细参考本文后续“1-3-1分析”) //yii\base\Module::setModules() public static function configure($object, $properties) { foreach ($properties as $name => $value) { $object->$name = $value; } return $object; } yii\base\Application::init()分析 public function init() { //设置当前应用状态 $this->state = self::STATE_INIT; //见下面的bootstrap() $this->bootstrap(); } protected function bootstrap() { //request是核心组件,这里使用了依赖注入机制(ServiceLocator)来获取request //注意:这里是第一次调用request组件,会初始化该组件 $request = $this->getRequest(); //设置别名 Yii::setAlias('@webroot', dirname($request->getScriptFile())); Yii::setAlias('@web', $request->getBaseUrl()); //跳转到yii\base\Application::bootstrap(),详细参考本文后续“1-3-2分析” //在这里主要是处理第三方扩展(extensions)和一些预启动(bootstrap) parent::bootstrap(); }
1-3-1分析:yii\di\ServiceLocator::setComponents()
public function setComponents($components) { foreach ($components as $id => $component) { //设置各个组件的定义,这样后续就能通过依赖注入机制来获取获取组件了 //应该还记得这里的配置参数来源于config文件中的定义和yii\web\Application::coreComponents()中的定义 $this->set($id, $component); } }
1-3-2分析:yii\base\Application::bootstrap()
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::trace('Bootstrap with ' . get_class($component) . '::bootstrap()', __METHOD__); $component->bootstrap($this); } else { Yii::trace('Bootstrap with ' . get_class($component), __METHOD__); } } } //预启动,通常会包括‘log’组件,‘debug’模块和‘gii’模块(参考配置文件) foreach ($this->bootstrap as $class) { $component = null; if (is_string($class)) { if ($this->has($class)) { $component = $this->get($class); } elseif ($this->hasModule($class)) { $component = $this->getModule($class); } elseif (strpos($class, '\\') === false) { throw new InvalidConfigException("Unknown bootstrapping component ID: $class"); } } if (!isset($component)) { $component = Yii::createObject($class); } if ($component instanceof BootstrapInterface) { Yii::trace('Bootstrap with ' . get_class($component) . '::bootstrap()', __METHOD__); $component->bootstrap($this); } else { Yii::trace('Bootstrap with ' . get_class($component), __METHOD__); } } }
在完成了上面的流程之后,应用就算启动成功了,可以开始运行,处理请求了。
运行应用:yii\web\Application::run()
分析:yii\base\Application::run()
在上面的构造函数执行完后,开始运行应用。即下面这行代码的run()部分
(new yii\web\Application($config))->run();//(实际上执行的是yii\base\Application::run())
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-1分析” $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; //发送响应,详细参考本文后续“2-2分析” $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-1分析: yii\web\Application::handleRequest()
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::trace("Route requested: '$route'", __METHOD__); //记录下当前请求的route $this->requestedRoute = $route; //执行路由相对应的action(yii\base\Module::runAction()详细参考本文后续“2-1-1分析”) $result = $this->runAction($route, $params); if ($result instanceof Response) { return $result; } else { //如果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); } }
2-1-1分析: yii\base\Module::runAction()
public function runAction($route, $params = []) { //根据路由创建Controller实例(详细参考本文后续“2-1-1-1分析”) $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()详细参考本文后续“2-1-1-2分析”) $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) . '".'); }
2-1-1-1分析: yii\base\Module::createController()
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可以如下 /* [ 'account' => 'app\controllers\UserController', 'article' => [ 'class' => 'app\controllers\PostController', 'pageTitle' => 'something new', ], ] */ // 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]; } 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 (!preg_match('%^[a-z][a-z0-9\\-_]*$%', $className)) { return null; } if ($prefix !== '' && !preg_match('%^[a-z0-9_/]+$%i', $prefix)) { return null; } //进行一些转换 $className = str_replace(' ', '', ucwords(str_replace('-', ' ', $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; }
2-1-1-2分析:yii\base\Controller::runAction()
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::trace('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开始,依次执行“模块级”的beforeActio() //如果有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()详细参考本文后续“2-1-1-2-1分析”和yii\base\Action::runWithParams()详细参考本文后续“2-1-1-2-2分析”) $result = $action->runWithParams($params); //执行当前Controller的afterAction $result = $this->afterAction($action, $result); //从当前模块开始,执行afterAction,直至所有祖先的afterAction // call afterAction on modules foreach ($modules as $module) { /* @var $module Module */ $result = $module->afterAction($action, $result); } } if ($oldAction !== null) { $this->action = $oldAction; } //如果有beforeAction没有通过,那么会返回null return $result; } public function createAction($id) { //默认action if ($id === '') { $id = $this->defaultAction; } //独立action(Standalone Actions ) $actionMap = $this->actions(); if (isset($actionMap[$id])) { //返回一个action实例,通常是yii\base\Action的子类 return Yii::createObject($actionMap[$id], [$id, $this]); } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id) && strpos($id, '--') === false && trim($id, '-') === $id) { $methodName = 'action' . str_replace(' ', '', ucwords(implode(' ', explode('-', $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; }
2-1-1-2-1分析: yii\base\InlineAction::runWithParams()
public function runWithParams($params) { //yii\web\Controller::bindActionParams()详细参考本文后续“2-1-1-2-1-1分析” $args = $this->controller->bindActionParams($this, $params); Yii::trace('Running action: ' . get_class($this->controller) . '::' . $this->actionMethod . '()', __METHOD__); if (Yii::$app->requestedParams === null) { Yii::$app->requestedParams = $args; } //真正地调用开发者写的Action代码 return call_user_func_array([$this->controller, $this->actionMethod], $args); }
2-1-1-2-2分析:yii\base\Action::runWithParams()
public function runWithParams($params) { if (!method_exists($this, 'run')) { throw new InvalidConfigException(get_class($this) . ' must define a "run()" method.'); } //yii\web\Controller::bindActionParams()详细参考本文后续“2-1-1-2-1-1分析” $args = $this->controller->bindActionParams($this, $params); Yii::trace('Running action: ' . get_class($this) . '::run()', __METHOD__); if (Yii::$app->requestedParams === null) { Yii::$app->requestedParams = $args; } if ($this->beforeRun()) { //执行独立Action的run方法 $result = call_user_func_array([$this, 'run'], $args); $this->afterRun(); return $result; } else { return null; } }
2-1-1-2-1-1分析:yii\web\Controller::bindActionParams()
public function bindActionParams($action, $params) { if ($action instanceof InlineAction) { //如果是InlineAction则对Controller中相应的action方法进行反射 $method = new \ReflectionMethod($this, $action->actionMethod); } else { //如果是独立action则对该Action类的run方法进行反射 $method = new \ReflectionMethod($action, 'run'); } $args = []; $missing = []; $actionParams = []; //通过php提供的反射机制绑定Action的参数,同时还会判断url中的参数是否满足要求 foreach ($method->getParameters() as $param) { $name = $param->getName(); if (array_key_exists($name, $params)) { if ($param->isArray()) { $args[] = $actionParams[$name] = (array) $params[$name]; } elseif (!is_array($params[$name])) { $args[] = $actionParams[$name] = $params[$name]; } else { throw new BadRequestHttpException(Yii::t('yii', 'Invalid data received for parameter "{param}".', [ 'param' => $name, ])); } unset($params[$name]); } elseif ($param->isDefaultValueAvailable()) { $args[] = $actionParams[$name] = $param->getDefaultValue(); } else { $missing[] = $name; } } if (!empty($missing)) { throw new BadRequestHttpException(Yii::t('yii', 'Missing required parameters: {params}', [ 'params' => implode(', ', $missing), ])); } $this->actionParams = $actionParams; return $args; }
2-2分析: yii\web\Response::send()
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; } protected function prepare() { if ($this->stream !== null) { return; } //使用formatter对相应进行处理,这个可以在配置文件中自定义 if (isset($this->formatters[$this->format])) { $formatter = $this->formatters[$this->format]; if (!is_object($formatter)) { $this->formatters[$this->format] = $formatter = Yii::createObject($formatter); } if ($formatter instanceof ResponseFormatterInterface) { $formatter->format($this); } else { throw new InvalidConfigException("The '{$this->format}' response formatter is invalid. It must implement the ResponseFormatterInterface."); } } elseif ($this->format === self::FORMAT_RAW) { if ($this->data !== null) { $this->content = $this->data; } } else { throw new InvalidConfigException("Unsupported response format: {$this->format}"); } //确保响应的content为string if (is_array($this->content)) { throw new InvalidParamException('Response content must not be an array.'); } elseif (is_object($this->content)) { if (method_exists($this->content, '__toString')) { $this->content = $this->content->__toString(); } else { throw new InvalidParamException('Response content must be a string or an object implementing __toString().'); } } } protected function sendHeaders() { //判断是否已经把头部发送出去了 if (headers_sent()) { return; } if ($this->_headers) { $headers = $this->getHeaders(); //设置并发送http响应头 foreach ($headers as $name => $values) { $name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $name))); // set replace for first occurrence of header but false afterwards to allow multiple $replace = true; foreach ($values as $value) { //主要就是调用了PHP的header()函数 header("$name: $value", $replace); $replace = false; } } } $statusCode = $this->getStatusCode(); header("HTTP/{$this->version} {$statusCode} {$this->statusText}"); //主要就是调用了PHP的setcookie()函数 $this->sendCookies(); } protected function sendContent() { if ($this->stream === null) { //直接echo输出内容 echo $this->content; return; } set_time_limit(0); // Reset time limit for big files $chunkSize = 8 * 1024 * 1024; // 8MB per chunk if (is_array($this->stream)) { list ($handle, $begin, $end) = $this->stream; fseek($handle, $begin); while (!feof($handle) && ($pos = ftell($handle)) <= $end) { if ($pos + $chunkSize > $end) { $chunkSize = $end - $pos + 1; } echo fread($handle, $chunkSize); flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit. } fclose($handle); } else { while (!feof($this->stream)) { echo fread($this->stream, $chunkSize); flush(); } fclose($this->stream); } }
请求流程
声明:以下内容转载自‘http://www.yiichina.com/doc/g...’
1.用户向入口脚本 web/index.php 发起请求。
2.入口脚本加载应用配置并创建一个应用实例去处理请求。
3.应用通过请求组件解析请求的路由。
4.应用创建一个控制器实例去处理请求。
5.控制器创建一个操作实例并针对操作执行过滤器。
6.如果任何一个过滤器返回失败,则操作退出。
7.如果所有过滤器都通过,操作将被执行。
8.操作会加载一个数据模型,或许是来自数据库。
9.操作会渲染一个视图,把数据模型提供给它。
10.渲染结果返回给响应组件。
11.响应组件发送渲染结果给用户浏览器。
应用主体生命周期
声明:以下内容转载自"http://www.yiichina.com/doc/guide/2.0/structure-applications"
在深入研究了源码之后,在此总结一下。当运行入口脚本处理请求时, 应用主体会经历以下生命周期:
- 入口脚本加载应用主体配置数组。
入口脚本创建一个应用主体实例:
a.调用 preInit() 配置几个高级别应用主体属性, 比如 yii\\base\\Application::basePath。 b.注册 yii\\base\\Application::errorHandler 错误处理方法. c.配置应用主体属性. d.调用 init() 初始化,该函数会调用 bootstrap() 运行引导启动组件.
入口脚本调用 yii\base\Application::run() 运行应用主体:
a.触发 EVENT_BEFORE_REQUEST 事件。 b.处理请求:解析请求 路由 和相关参数; 创建路由指定的模块、控制器和动作对应的类,并运行动作。 c.触发 EVENT_AFTER_REQUEST 事件。 d.发送响应到终端用户.
- 入口脚本接收应用主体传来的退出状态并完成请求的处理。