Yii2 源码分析 - 入口文件执行流程

以 yii 2.0.14 高级版的 frontend 为例,从 frontend/web/index.php 开始

代码语言:javascript
复制
//引用 yii2 composer 的 autoload,调用 getLoader
require __DIR__ . '/../../vendor/autoload.php';
//引用 yii.php
require __DIR__ . '/../../vendor/yiisoft/yii2/Yii.php';
//引用 bootstrap.php 定义一些别名等
require __DIR__ . '/../../common/config/bootstrap.php';
require __DIR__ . '/../config/bootstrap.php';

//合并配置文件
$config = yii\helpers\ArrayHelper::merge(
require DIR . '/../../common/config/main.php',
require DIR . '/../../common/config/main-local.php',
require DIR . '/../config/main.php',
require DIR . '/../config/main-local.php'
);

(new yii\web\Application($config))->run();

入口文件看着就这么几行,简单的很,那他是怎么通过这几行来运行应用的呢?先看 Yii.php 内的逻辑

代码语言:javascript
复制
/**

  • Yii::autoload 内执行过程

  • 1、先查看类是否在 Yii::$classMap 中存在,存在直接调用 getAlias 生成类文件物理地址

  • 2、如果 Yii::$classMap 中不存在,将命名空间转为实际路径调用 getAlias 生成类文件物理地址
    /
    spl_autoload_register(['Yii', 'autoload'], true, true);
    //yii2 核心类的类名和物理文件地址映射的 hash 数组
    Yii::$classMap = require DIR . '/classes.php';
    /
    *

  • 实例化 依赖注入(Dependency Injection,DI)容器

  • 依赖注入容器知道怎样初始化并配置对象及其依赖的所有对象

  • 在Yii中使用DI解耦,有2种注入方式:构造函数注入、属性注入

  • yii\di\Container 继承了

  • yii\base\Component

  • yii\base\BaseObject

  • BaseObject 实现了 Configurable

  • DI容器只支持 yii\base\Object 类

  • 如果你的类想放在DI容器里,那么必须继承自 yii\base\Object 类

  • 参考地址:

  • http://www.digpage.com/di.html

  • https://www.cnblogs.com/minirice/p/yii2_configurations.html
    /
    Yii::$container = new yii\di\Container();

接下来,就是重头戏,yii\web\Application,它继承了
yii\base\Application
yii\base\Module
yii\di\ServiceLocator(服务定位器)
yii\base\Component
yii\base\BaseObject, BaseObject 实现 Configurable
PS:继承 Component 的都有 on event 和 as behavior 配置实现事件绑定

一、new yii\web\Application 时,会调用构造方法 yii\base\Application::__construct
代码语言:javascript
复制
public function __construct($config = [])
{
Yii::app = this;
//application 对象放到注册树中
static::setInstance($this);
$this->state = self::STATE_BEGIN;
/
*

  • 初始化 application 中应用属性的一些值,配置一些高优先级的应用属性
  • 还会初始化 components 中,log、user、urlManager 对应的类文件
  • 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'];
    
  • }
    
  • }
  • yii\web\Application 中,coreComponents 的代码
  • public function coreComponents()
  • {
  • return array_merge(parent::coreComponents(), [
    
  •     'request' => ['class' => 'yii\web\Request'],
    
  •     'response' => ['class' => 'yii\web\Response'],
    
  •     'session' => ['class' => 'yii\web\Session'],
    
  •     'user' => ['class' => 'yii\web\User'],
    
  •     'errorHandler' => ['class' => 'yii\web\ErrorHandler'],
    
  • ]);
    
  • }
  • yii\base\Application 中,coreComponents 的代码
  • public function 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'],
    
  • ];
    
  • }
  • 从2.0.11 开始,配置支持使用 container 属性来配置依赖注入容器
  • 'container' => [
  • 'definitions' => [
    
  •     'yii\widgets\LinkPager' => ['maxButtonCount' => 5]
    
  • ],
    
  • 'singletons' => [
    
  •     // 依赖注入容器单例配置
    
  • ]
    
  • ]

/
this->preInit(config);
/
*

  • registerErrorHandler 内代码

  • 1、调用 this->set('errorHandler', config['components']['errorHandler'])

  • 将 errorHandler 配置放到 ServiceLocator (_definitions 数组中,这时还没实例化)

  • 2、调用 $this->getErrorHandler()->register()

  • 调用 getErrorHandler,使用 createObject 调用 Container 依赖注入容器实例化对象

  • 调用 yii\web\ErrorHandler::register,初始化错误异常显示和抛出
    /
    this->registerErrorHandler(config);
    /
    *

  • 在多层继承中,调用上级某一层的构造函数,而不是单纯的父类构造函数

  • 上级某一层的构造函数中如果调用了某个方法

  • 并且这个方法被下层类重写过,那么会直接执行重写之后的方法

  • 所以执行 Component::__construct,__construct 中调用 init()

  • 会执行 yii\base\Application 的 init

  • 如果上级调用下级重写的 静态方法 时

  • 要使用延时静态绑定(上级静态调用 self::a() 改为 static::a())
    /
    Component::__construct($config);
    }

二、yii\base\Application::init 代码
代码语言:javascript
复制
public function init()
{
$this->state = self::STATE_INIT;
$this->bootstrap();
}
三、yii\web\Application::bootstrap 代码
代码语言:javascript
复制
protected function bootstrap()
{
/
*

  • 通过 Application::get('request')

  • 使用 createObject 实现调用 Container 依赖注入容器实例化对象
    /
    request = this->getRequest();
    //定义别名
    Yii::setAlias('@webroot', dirname($request->getScriptFile()));
    Yii::setAlias('@web', $request->getBaseUrl());
    //调用 yii\base\Application::bootstrap 代码
    parent::bootstrap();
    }

  • 四、yii\base\Application::bootstrap 代码太多,不展示源码了,大致总结为

    1、是否在配置文件中配置了 extensions 参数,如果没有配置,直接加载扩展清单文件 @vendor/yiisoft/extensions.php,否则使用配置的 extensions。然后在 extensions 文件返回的数组中,可有含有 alias 和 bootstrap 参数,根据 alias 中的参数定义别名,根据 bootstrap 中的参数,使用 createObject 实例化对象(创建并运行各个扩展声明的 引导组件 )
    2、根据配置文件配置的 bootstrap 参数,使用 createObject 实例化对象(创建并运行各个 应用组件 以及在应用的 bootstrap 属性中声明的各个 模块组件 )
    3、注意:extensions 文件中配置的 bootstrap 和 配置文件中配置的 bootstrap,如果实现了 BootstrapInterface 接口,还会执行实例化后的 bootstrap 方法
    4、注意:bootstrap 会直接将配置的类实例化,而不是在第一次使用的时候实例化,所以为了性能考虑 bootstrap 中的配置应该尽量少,而且只配置一些全局使用的类

    五、yii\base\Application::run 代码
    代码语言:javascript
    复制
    public function run()
    {
    try {
    $this->state = self::STATE_BEFORE_REQUEST;
    /
    *

    • trigger 触发通知,将此事件通知给绑定到这个事件的观察者,绑定事件的方法:
    • yii\base\Component 或者其子类::on("事件名称","方法")
      */
      $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;
    } catch (ExitException $e) {
    this->end(e->statusCode, isset(response) ? response : null);
    return $e->statusCode;
    }
    }

    六、yii\web\Application::handleRequest 代码
    代码语言:javascript
    复制
    public function handleRequest($request)
    {
    if (empty($this->catchAll)) {
    try {
    //resolve 方法调用 urlManager 对 url 进行解析
    list(route, params) = $request->resolve();
    } catch (UrlNormalizerRedirectException $e) {
    url = e->url;
    if (is_array($url)) {
    if (isset($url[0])) {
    url[0] = '/' . ltrim(url[0], '/');
    }
    url += request->getQueryParams();
    }

     return $this->getResponse()->redirect(Url::to($url, $e->scheme), $e->statusCode);
    

    }
    } else {
    /**

    • 如果设置了 catchAll 变量, 那么所有请求都会跳转到这里
    • 示例:
    • 假设网站维护, 需要将网站重定向到一个设置好的页面上
    • 可以在配置文件中添加
    • 'catchAll' => ['offline/index']
    • 这样, 所有的访问都跳转到 offline/index 页面了
      */
      route = this->catchAll[0];
      params = this->catchAll;
      unset($params[0]);
      }
      try {
      Yii::debug("Route requested: '$route'", METHOD);
      this->requestedRoute = route;
      //根据 route 访问对应的 module/controller/action
      result = this->runAction(route, params);
      if ($result instanceof Response) {
      return $result;
      }

    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);
    }
    }

    七、yii\base\Module::runAction 代码
    代码语言:javascript
    复制
    public function runAction(route, params = [])
    {
    /**

  • yii\base\Module::createController 代码也不贴了,可以追进去看,思路是

  • 1、如果 route 是空(直接通过域名访问应用 www.aaa.com)

  • 使用配置中的 defaultRoute 属性

  • 2、route 不为空,查看配置文件中是否有 controllerMap 的配置

  • 直接使用配置创建

  • controllerMap 配置如

  • [

  • 'controllerMap' => [
    
  •     // 用类名申明 "account" 控制器
    
  •     'account' => 'app\controllers\UserController',
    
  •     // 用配置数组申明 "article" 控制器
    
  •     'article' => [
    
  •         'class' => 'app\controllers\PostController',
    
  •         'enableCsrfValidation' => false,
    
  •     ]
    
  • ]
    
  • ]

  • 3、调用 yii/base/Module::getModule 查看 route 中是否有 module 存在

  • 如果直接调用yii/base/Module::createController 方法

  • 否则调用 yii/base/Module::createControllerByID

  • 通过 createControllerByID 实例化的 Controller 类,必须继承 yii\base\Controller

  • createController 和 createControllerByID 都使用 Yii::createObject 实例化
    */
    parts = this->createController($route);
    if (is_array($parts)) {
    list(controller, actionID) = $parts;
    oldController = Yii::app->controller;
    Yii::app->controller = controller;
    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) . '".');
    }

    八、说明一下 yii/base/Module::getModule 这个很有意思

    1、先看一下配置文件时 modules 配置后的赋值过程
    我们使用 modules 时,需要在配置文件中配置 modules,比如

    代码语言:javascript
    复制
    'modules' => [
    'v1' => [
    'class' => 'frontend\modules\v1\Module',
    ],
    ],

    或者像 main-local.php 中那样,新建一个 config,配置完以后 return config,config 中配置</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">config['modules']['gii'] = [
    'class' => 'yii\gii\Module',
    ];

    这个 modules 的属性,在 Application 及其父类中,都是不存在的
    只有私有属性 $_modules,存在于 yii\base\Module 类中
    当 new yii\web\Application 执行 yii\base\Application::construct 方法时
    方法中执行了 Component::construct($config) (不清楚的往上看,上边有这块代码)
    然后 Component::construct($config) 实际执行的是
    BaseObject::construct(config) ,然后方法中执行</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">if (!empty(config)) {
    Yii::configure(this, config);
    }

    再调用 yii\base\Component::setter 方法 (yii\base\Module::setModules),将 $_modules 赋值
    2、如果 module 套着 module,需要这么这么设置

    代码语言:javascript
    复制
    'modules' => [
    'v1' => [
    'class' => 'frontend\modules\v1\Module',
    'modules' => [
    'v2' => 'frontend\modules\v2\Module'
    ],
    ],
    ],
    九、yii\base\Controller::runAction 代码
    代码语言:javascript
    复制
    public function runAction(id, params = [])
    {
    /**

    • yii\base\Controller::createAction 代码也不贴了,可以追进去看,思路是
    • 1、如果 action id 是空(访问 www.aaa.com/controller)
    • 使用 yii\base\Controller 中的 defaultAction 属性
    • 2、id 不为空,查看 Controller::actions 方法中是否有配置
    • 如果有,直接使用配置创建,actions 配置如
    • public function actions()
    • {
    • return [
      
    •     &#39;error&#39; =&gt; [
      
    •         &#39;class&#39; =&gt; &#39;yii\web\ErrorAction&#39;,
      
    •     ],
      
    •     &#39;captcha&#39; =&gt; [
      
    •         &#39;class&#39; =&gt; &#39;yii\captcha\CaptchaAction&#39;,
      
    •         &#39;fixedVerifyCode&#39; =&gt; YII_ENV_TEST ? &#39;testme&#39; : null,
      
    •     ],
      
    • ];
      
    • }
    • 3、利用反射(ReflectionMethod)查看调用方法是否存在,是否是公共方法
    • 如果是,返回 yii\base\InlineAction 的实例
      */
      action = this->createAction($id);
      if ($action === null) {
      throw new InvalidRouteException('Unable to resolve the request: ' . this-&gt;getUniqueId() . &#39;/&#39; . id);
      }
      Yii::debug('Route to run: ' . $action->getUniqueId(), METHOD);
      if (Yii::$app->requestedAction === null) {
      Yii::app-&gt;requestedAction = action;
      }

    oldAction = this->action;
    this-&gt;action = action;

    $modules = [];
    $runAction = true;
    //调用所有加载模块中的 beforeAction 方法
    foreach (this-&gt;getModules() as module) {
    if (module-&gt;beforeAction(action)) {
    array_unshift(modules, module);
    } else {
    $runAction = false;
    break;
    }
    }

    $result = null;

    if (runAction &amp;&amp; this->beforeAction($action)) {

     $result = $action-&gt;runWithParams($params);
    
     $result = $this-&gt;afterAction($action, $result);
    
     //调用所有加载模块中的 afterAction 方法
     foreach ($modules as $module) {
         $result = $module-&gt;afterAction($action, $result);
     }
    

    }

    if ($oldAction !== null) {
    this-&gt;action = oldAction;
    }
    return $result;
    }

    最后,附个图,源自
    http://www.yiichina.com/doc/guide/2.0/structure-applications

    application-lifecycle.png

    G

    M

    T

    Detect languageAfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBengaliBosnianBulgarianCatalanCebuanoChichewaChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDutchEnglishEsperantoEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekGujaratiHaitian CreoleHausaHebrewHindiHmongHungarianIcelandicIgboIndonesianIrishItalianJapaneseJavaneseKannadaKazakhKhmerKoreanLaoLatinLatvianLithuanianMacedonianMalagasyMalayMalayalamMalteseMaoriMarathiMongolianMyanmar (Burmese)NepaliNorwegianPersianPolishPortuguesePunjabiRomanianRussianSerbianSesothoSinhalaSlovakSlovenianSomaliSpanishSundaneseSwahiliSwedishTajikTamilTeluguThaiTurkishUkrainianUrduUzbekVietnameseWelshYiddishYorubaZulu

    AfrikaansAlbanianArabicArmenianAzerbaijaniBasqueBelarusianBengaliBosnianBulgarianCatalanCebuanoChichewaChinese (Simplified)Chinese (Traditional)CroatianCzechDanishDutchEnglishEsperantoEstonianFilipinoFinnishFrenchGalicianGeorgianGermanGreekGujaratiHaitian CreoleHausaHebrewHindiHmongHungarianIcelandicIgboIndonesianIrishItalianJapaneseJavaneseKannadaKazakhKhmerKoreanLaoLatinLatvianLithuanianMacedonianMalagasyMalayMalayalamMalteseMaoriMarathiMongolianMyanmar (Burmese)NepaliNorwegianPersianPolishPortuguesePunjabiRomanianRussianSerbianSesothoSinhalaSlovakSlovenianSomaliSpanishSundaneseSwahiliSwedishTajikTamilTeluguThaiTurkishUkrainianUrduUzbekVietnameseWelshYiddishYorubaZulu

    Text-to-speech function is limited to 200 characters

    Options : History : Feedback : Donate

    Close