扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
用yii2框架用了将近2年,一直都没有去看过它底层源码, 马上快不用了,最近对其源码研究一番,哈哈
公司主营业务:成都做网站、成都网站设计、移动网站开发等业务。帮助企业客户真正实现互联网宣传,提高企业的竞争能力。创新互联是一支青春激扬、勤奋敬业、活力青春激扬、勤奋敬业、活力澎湃、和谐高效的团队。公司秉承以“开放、自由、严谨、自律”为核心的企业文化,感谢他们对我们的高要求,感谢他们从不同领域给我们带来的挑战,让我们激情的团队有机会用头脑与智慧不断的给客户带来惊喜。创新互联推出鼓楼免费做网站回馈大家。
废话少说,上代码,
入口文件是web/index.php
run();
解释1-1
直接上Yii.php文件源码
解释1-2
我们最关键的点来了分析application启动流程
首先看看Application构造函数
首先进入yii\web\Application类,发现没有构造方法,于是跟踪它的层级关系,列出来:
yii\web\Application -> \yii\base\Application -> \yii\base\Module -> \yii\di\ServiceLocator -> \yii\base\Component
-> \yii\base\BaseObject -> \yii\base\Configurable(接口interface)
首先进入yii\base\Application找到__construct方法:
public function __construct($config = []) { //保存当前启动的application实例 Yii::$app = $this; //将Yii::$app->loadedModules[实例类名] = 当前实例; $this->setInstance($this); $this->state = self::STATE_BEGIN; //见解释1-2-1 $this->preInit($config); //见解释1-2-2 $this->registerErrorHandler($config); //见解释1-2-3 Component::__construct($config); }解释1-2-1:
/* 该函数作用是将配置数组进一步合并完善数组中的key $config即为入口文件包含到的config/web.php返回的数组,举例如下: $config = [ 'id' => 'basic', 'basePath' => dirname(__DIR__), 'bootstrap' => ['log'], 'aliases' => [ '@bower' => '@vendor/bower-asset', '@npm' => '@vendor/npm-asset', ], 'components' => [ 'request' => [ // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation 'cookieValidationKey' => '', ], 'cache' => [ 'class' => 'yii\caching\FileCache', ], 'user' => [ 'identityClass' => 'app\models\User', 'enableAutoLogin' => true, ], 'errorHandler' => [ 'errorAction' => 'site/error', ], 'mailer' => [ 'class' => 'yii\swiftmailer\Mailer', 'useFileTransport' => true, ], 'log' => [ 'traceLevel' => YII_DEBUG ? 3 : 0, 'targets' => [ [ 'class' => 'yii\log\FileTarget', 'levels' => ['error', 'warning'], ], ], ], 'db' => $db, ], 'params' => $params, ]; */ public function preInit(&$config) { if (!isset($config['id'])) { throw new InvalidConfigException('The "id" configuration for the Application is required.'); } if (isset($config['basePath'])) { $this->setBasePath($config['basePath']); unset($config['basePath']); } else { throw new InvalidConfigException('The "basePath" configuration for the Application is required.'); } if (isset($config['vendorPath'])) { $this->setVendorPath($config['vendorPath']); unset($config['vendorPath']); } else { $this->getVendorPath(); } if (isset($config['runtimePath'])) { $this->setRuntimePath($config['runtimePath']); unset($config['runtimePath']); } else { // set "@runtime" $this->getRuntimePath(); } //设置时区 if (isset($config['timeZone'])) { $this->setTimeZone($config['timeZone']); unset($config['timeZone']); } elseif (!ini_get('date.timezone')) { $this->setTimeZone('UTC'); } if (isset($config['container'])) { $this->setContainer($config['container']); unset($config['container']); } /* coreComponents返回核心组件 return [ 'log' => ['class' => 'yii\log\Dispatcher'], 'view' => ['class' => 'yii\web\View'], 'formatter' => ['class' => 'yii\i18n\Formatter'], 'i18n' => ['class' => 'yii\i18n\I18N'], 'mailer' => ['class' => 'yii\swiftmailer\Mailer'], 'urlManager' => ['class' => 'yii\web\UrlManager'], 'assetManager' => ['class' => 'yii\web\AssetManager'], 'security' => ['class' => 'yii\base\Security'], ]; 合并配置文件数组的components key内容 */ foreach ($this->coreComponents() as $id => $component) { if (!isset($config['components'][$id])) { $config['components'][$id] = $component; } elseif (is_array($config['components'][$id]) && !isset($config['components'][$id]['class'])) { $config['components'][$id]['class'] = $component['class']; } } }解释1-2-2:
protected function registerErrorHandler(&$config) { //YII_ENABLE_ERROR_HANDLER可以在文件中配,默认为true if (YII_ENABLE_ERROR_HANDLER) { if (!isset($config['components']['errorHandler']['class'])) { echo "Error: no errorHandler component is configured.\n"; exit(1); } /* 晒个默认配置 'errorHandler' => [ 'errorAction' => 'site/error', ], $this->set方法是引自\yii\di\ServiceLocator的set方法, 注册组件$this->_definitions['erroHandler'] = ['errorAction' => 'site/error','class'=>'yii\web\ErrorHandler']; */ $this->set('errorHandler', $config['components']['errorHandler']); unset($config['components']['errorHandler']); //这个方法会实例化errorHandler的class,实例化这步实际上用到依赖注入,之前我已经讲过一点,以后写个yii2创建对象流程 //并将实例化的对象保存到$this->__components['errorHandler'] $this->getErrorHandler()->register(); } }解释1-2-3:
//实际调用的是yii\base\BaseObject类的构造方法 public function __construct($config = []) { if (!empty($config)) { //将$config数组中的每个key都赋值$this->本地化变量 Yii::configure($this, $config); } $this->init(); }很明显追踪$this->init()方法,后面追踪到yii\base\Application的init方法。
public function init() { $this->state = self::STATE_INIT; $this->bootstrap(); }再看看bootstrap方法
先看看yii\web\Application的bootstrap方法
protected function bootstrap() { //获得request对象实例 $request = $this->getRequest(); Yii::setAlias('@webroot', dirname($request->getScriptFile())); Yii::setAlias('@web', $request->getBaseUrl()); parent::bootstrap(); }再看看yii\base\Application的bootstrap方法
protected function bootstrap() { if ($this->extensions === null) { $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::debug('Bootstrap with ' . get_class($component) . '::bootstrap()', __METHOD__); $component->bootstrap($this); } else { Yii::debug('Bootstrap with ' . get_class($component), __METHOD__); } } } //已配置需要初始化的组件初始化 foreach ($this->bootstrap as $mixed) { $component = null; if ($mixed instanceof \Closure) { Yii::debug('Bootstrap with Closure', __METHOD__); if (!$component = call_user_func($mixed, $this)) { continue; } } elseif (is_string($mixed)) { if ($this->has($mixed)) { $component = $this->get($mixed); } elseif ($this->hasModule($mixed)) { $component = $this->getModule($mixed); } elseif (strpos($mixed, '\\') === false) { throw new InvalidConfigException("Unknown bootstrapping component ID: $mixed"); } } if (!isset($component)) { $component = Yii::createObject($mixed); } if ($component instanceof BootstrapInterface) { Yii::debug('Bootstrap with ' . get_class($component) . '::bootstrap()', __METHOD__); $component->bootstrap($this); } else { Yii::debug('Bootstrap with ' . get_class($component), __METHOD__); } } }到此new Application($config)这一步分析完毕
再来看看$app->run()做了什么
先打开yii\base\Application的run方法
public function run() { try { $this->state = self::STATE_BEFORE_REQUEST; //这里可以绑定自定义事件,类似钩子 $this->trigger(self::EVENT_BEFORE_REQUEST); $this->state = self::STATE_HANDLING_REQUEST; //最重要的一点 见解释2-1 $response = $this->handleRequest($this->getRequest()); $this->state = self::STATE_AFTER_REQUEST; $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
//$request为yii\web\Request类的实例 public function handleRequest($request) { if (empty($this->catchAll)) { try { 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 { $route = $this->catchAll[0]; $params = $this->catchAll; unset($params[0]); } try { Yii::debug("Route requested: '$route'", __METHOD__); $this->requestedRoute = $route; /* 例如访问url为http://domain/web/index.php?r=post/index&id=3 $route为路由url字符串,得到post/index $params为Query String数组,得到['id'=>3, 'r'=> 'post/index'] $result的值为对应conroller执行对应action返回的值或者对象 */ $result = $this->runAction($route, $params); if ($result instanceof Response) { return $result; } //构造一个Response对象 $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); } }我们进入$this->runAction看看
public function runAction($route, $params = []) { //得到($controller实例对象和action名称的字符串) $parts = $this->createController($route); if (is_array($parts)) { /* @var $controller Controller */ list($controller, $actionID) = $parts; $oldController = Yii::$app->controller; Yii::$app->controller = $controller; //执行controller的对应的actionID方法,该方法返回的内容赋值给$result $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-2:
打开yii\web\Response的send方法
public function send() { if ($this->isSent) { return; } $this->trigger(self::EVENT_BEFORE_SEND); //取得$response对象的format再获得该format对象的实例执行format方法(就是header设置Content-Type) //见2-2-1 $this->prepare(); $this->trigger(self::EVENT_AFTER_PREPARE); //见2-2-2 $this->sendHeaders(); //见2-2-3 $this->sendContent(); $this->trigger(self::EVENT_AFTER_SEND); $this->isSent = true; }解释2-2-1:
protected function prepare() { if ($this->stream !== null) { return; } 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}"); } if (is_array($this->content)) { throw new InvalidArgumentException('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 InvalidArgumentException('Response content must be a string or an object implementing __toString().'); } } }解释2-2-2:
protected function sendHeaders() { if (headers_sent($file, $line)) { throw new HeadersAlreadySentException($file, $line); } if ($this->_headers) { $headers = $this->getHeaders(); 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) { header("$name: $value", $replace); $replace = false; } } } $statusCode = $this->getStatusCode(); header("HTTP/{$this->version} {$statusCode} {$this->statusText}"); $this->sendCookies(); }这里补充下sendCookies方法:
protected function sendCookies() { if ($this->_cookies === null) { return; } $request = Yii::$app->getRequest(); if ($request->enableCookieValidation) { if ($request->cookieValidationKey == '') { throw new InvalidConfigException(get_class($request) . '::cookieValidationKey must be configured with a secret key.'); } $validationKey = $request->cookieValidationKey; } foreach ($this->getCookies() as $cookie) { $value = $cookie->value; if ($cookie->expire != 1 && isset($validationKey)) { $value = Yii::$app->getSecurity()->hashData(serialize([$cookie->name, $value]), $validationKey); } setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly); } }解释2-2-3:
protected function sendContent() { if ($this->stream === null) { 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); } }至此源码整个流程分析基本完毕,有些地方可能分析不够详细,后续再详细补充。
最后附加下官网文档的部分内容帮助大家理解
请求生命周期
以下图表展示了一个应用如何处理请求:
用户向入口脚本
web/index.php
发起请求。入口脚本加载应用配置并创建一个应用 实例去处理请求。
应用通过请求组件解析请求的 路由。
应用创建一个控制器实例去处理请求。
控制器创建一个动作实例并针对操作执行过滤器。
如果任何一个过滤器返回失败,则动作取消。
如果所有过滤器都通过,动作将被执行。
动作会加载一个数据模型,或许是来自数据库。
动作会渲染一个视图,把数据模型提供给它。
渲染结果返回给响应组件。
响应组件发送渲染结果给用户浏览器。
分享题目:yii2源码分析之执行基本流程
标题网址:http://kswjz.com/article/piigcs.html
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流