本文主要分析Yii2應用的啓動、運行的過程,主要包括如下三部分:入口腳本、啓動應用、運行應用。
在分析源碼的過程當中主要藉助了Xdebug工具。php
文件位置:web\index.phpweb
//定義全局變量 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()數據庫
分析:new yii\web\Application($config)
主要就是執行構造函數(代碼位置:vendor\yiisoft\yii2\base\Application.php)npm
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)bootstrap
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)segmentfault
//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()yii2
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__); } } }
在完成了上面的流程以後,應用就算啓動成功了,能夠開始運行,處理請求了。cookie
分析: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.發送響應到終端用戶.