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

最近在看 yii2 源码,想在入门的基础上掌握 yii2 更多的设计思路和设计风格,以便更好的理解 yii2
以 yii 2.0.14 高级版的 frontend 为例,从 frontend/web/index.php 开始

//引用 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 内的逻辑

/**
 * 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

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 代码

public function init()
{
    $this->state = self::STATE_INIT;
    $this->bootstrap();
}

三、yii\web\Application::bootstrap 代码

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 代码

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 代码

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 代码

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,比如

'modules' => [
    'v1' => [
        'class' => 'frontend\modules\v1\Module',
    ],
],

或者像 main-local.php 中那样,新建一个 config,配置完以后 returnconfig,$config 中配置

$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) ,然后方法中执行

if (!empty($config)) {
    Yii::configure($this, $config);
}

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

'modules' => [
    'v1' => [
        'class'         => 'frontend\modules\v1\Module',
        'modules'   => [
            'v2'    => 'frontend\modules\v2\Module'
        ],
    ],
],

九、yii\base\Controller::runAction 代码

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 [
     *         'error' => [
     *             'class' => 'yii\web\ErrorAction',
     *         ],
     *         'captcha' => [
     *             'class' => 'yii\captcha\CaptchaAction',
     *             'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
     *         ],
     *     ];
     * }
     * 
     * 3、利用反射(ReflectionMethod)查看调用方法是否存在,是否是公共方法
     * 如果是,返回 yii\base\InlineAction 的实例 
     */
    $action = $this->createAction($id);
    if ($action === null) {
        throw new InvalidRouteException('Unable to resolve the request: ' . $this->getUniqueId() . '/' . $id);
    }
    Yii::debug('Route to run: ' . $action->getUniqueId(), __METHOD__);
    if (Yii::$app->requestedAction === null) {
        Yii::$app->requestedAction = $action;
    }

    $oldAction = $this->action;
    $this->action = $action;

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

    $result = null;

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

        $result = $action->runWithParams($params);

        $result = $this->afterAction($action, $result);

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

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

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

流程图
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容