前面花了点时间给大家补充了一点php的基础,希望对各位有所帮助。后面我们还会再介绍yii2内部是如何实现这些的。
安装yii2
今天的内容比较轻松,我们开始着手yii2源码部分的讲解。
为了讲解的更全面化,我们还是准备从安装开始。
安装的详细步骤可参考这篇文章,这里我们直接用composer创建并快速初始化1
2
3
4
5composer create-project yiisoft/yii2-app-advanced yii2-analysis 2.0.12
cd yii2-analysis
php init
Your choice [0-1, or "q" to quit] 0
Initialize the application under 'Development' environment? [yes|no] yes
以上步骤,一步一步回车即可。
说明:这里我们选择最新的release版本(release 2.0.12),也希望各位避免差异化,尽量以此版本为准进行学习。
项目初始化之后,我们配置frontend和backend两个应用,其中域名 frontend.analysis.com 指向 yii2-analysis/frontend/web 目录, backend.analysis.com 指向 yii2-analysis/backend/web 目录。
配置成功的标识是这两个站点均可正常访问。
我们准备先从frontend应用说起。
源码位置
首先,我们需要知道yii2的源码在什么位置。
yii2的核心源码,位于 vendor/yiisoft/yii2 目录,我们打开 vendor/yiisoft/yii2/composer.json 可以看到有这么一项配置1
2
3"autoload": {
"psr-4": {"yii\\": ""}
},
所以,以后我们凡是说 yii\xxx\yyy, 各位就可以找到对应的源码文件 vendor/yiisoft/yii2/xxx/yyy.php,比如 yii\base\Application 指的就是 vendor/yiisoft/yii2/base/Application.php。
这个是 composer psr-4 规范的内容,多说了两句。
入口脚本分析
下面我们从应用的入口文件开始说起吧。
入口文件:frontend/web/index.php
为什么称为入口文件,frontend应用有几个入口文件呢?
按照web server的配置,入口文件是应用的开始,有且只有一个。
比如,以nginx为例我们是这样配置的:1
2
3location / {
try_files $uri $uri/ /index.php$is_args$args;
}
然你也可以叫做index2.php,index3.php,只要nginx这里的try_files配置好就没关系。
又说了些废话 T_T..
我们假设 frontend 应用是一套未知的某框架,nginx把一个请求转发过来,我们的应用程序需要做些什么呢?
最简单最没意义的,我们可以创建一个单文件,直接 echo ‘hello world’; 。
后面我们便可以在这个基础之上进行扩展,比如初始化配置,处理请求,解析路由,连接mysql等等,这便是框架要做的工作,并没有什么神奇之处。
我们看看入口文件的内容,注意看注释部分1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// 定义当前应用是否处于debug模式,即调试模式
defined('YII_DEBUG') or define('YII_DEBUG', true);
// 定义当前应用的运行环境,比如开发环境,测试环境和生产环境等
defined('YII_ENV') or define('YII_ENV', 'dev');
// 加载composer的authload文件
require(__DIR__ . '/../../vendor/autoload.php');
// 加载基础类文件
require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');
// 在项目启动之前,加载一些全局的配置
require(__DIR__ . '/../../common/config/bootstrap.php');
require(__DIR__ . '/../config/bootstrap.php');
// 加载应用配置,并通过 yii\helpers\ArrayHelper::merge 方法合并处理
$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')
);
// 实例化 yii\web\Application,并调用 run 方法运行
(new yii\web\Application($config))->run();
Yii.php
上面整体粗略的介绍了一下入口文件都做了什么,下面我们再来详细的看一看
首先加载 composer的autoload文件,以后凡是通过composer安装的第三方库,我们都可以直接调用。
接着我们来看一下引入的 vendor/yiisoft/yii2/Yii.php,这个文件算是我们接触源码的第一个文件,其内容如下1
2
3
4
5
6
7
8
9require(__DIR__ . '/BaseYii.php');
class Yii extends \yii\BaseYii
{
}
spl_autoload_register(['Yii', 'autoload'], true, true);
Yii::$classMap = require(__DIR__ . '/classes.php');
Yii::$container = new yii\di\Container();
可以看出这是一个继承 \yii\BaseYii 类的子类。等等,为什么这里可以直接以命名空间的方式写 \yii\BaseYii ?看上面 require(DIR . ‘/BaseYii.php’); 我们就猜到了原因,namespace yii; 在该文件内被声明。
继续看 Yii.php,Yii 类继承 \yii\BaseYii 之后,啥也没做,是个空类。
尽管如此,我们引用 BaseYii 的属性和方法时,最好还是用 Yii 来引用。比如我们可以用 Yii::getVersion() 获取当前yii2的版本。
自动加载机制
用过yii2那么久了,不知道各位有没有怀疑过一个问题:假如我们在 frontend/models 目录下创建了一个 UserModel , 为啥你没有手动 require UserModel.php 就可以直接 new UserModel 了呢?
没错,Yii.php这里的 spl_autoload_register 函数定义了 Yii::autoload 为自动加载方法,即 yii\BaseYii::autoload 方法。这个方法是 yii2 内置的自动加载器,当我们new一个找不到的类的时候,这个方法就会被自动调用。这是 spl_autoload_register 函数的作用。
我们来看一下yii2自动加载方法 BaseYii::autoload 的实现。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public static function autoload($className)
{
if (isset(static::$classMap[$className])) {
$classFile = static::$classMap[$className];
if ($classFile[0] === '@') {
$classFile = static::getAlias($classFile);
}
} elseif (strpos($className, '\\') !== false) {
$classFile = static::getAlias('@' . str_replace('\\', '/', $className) . '.php', false);
if ($classFile === false || !is_file($classFile)) {
return;
}
} else {
return;
}
include($classFile);
// 省略其他代码
}
从这一行代码 include($classFile); 便知晓,该方法的核心就是把我们声明的未找到的类文件 include 进来,这个是该方法的关键。
但是include的文件路径从哪里获取呢?我们只知道类以及其所在的命名空间。
继续看Yii.php, spl_autoload_register函数后面有这样一行代码1
2
3
4
5
6
7
8
9Yii::$classMap = require(__DIR__ . '/classes.php');
Yii::$classMap 属性就保存着yii2核心类和其类文件所属路径的映射关系,即 vendor/yiisoft/yii2/classes.php,不妨打开看一眼
return [
'yii\base\Action' => YII2_PATH . '/base/Action.php',
'yii\base\ActionEvent' => YII2_PATH . '/base/ActionEvent.php',
'yii\base\ActionFilter' => YII2_PATH . '/base/ActionFilter.php',
// ...
];
YII2_PATH 在 BaseYii.php 内,class声明的上面定义如下1
defined('YII2_PATH') or define('YII2_PATH', __DIR__);
回到 Yii::autoload 方法,注意第一行代码 if (isset(static::$classMap[$className])) {},其含义是如果 new 未找到的类是 yii2 内置的核心类,便可以快速根据类名找到其对应的路径,通过include加载进来,速度是很快的。
别名Alias
对于非yii2内置的核心类,比如 frontend\models\UserModel (当然该类只是我们假设的,并未实际创建,不要跟我说你怎么没找到这个文件) 。当我们在应用内实例化该类的时候,从 Yii::autoload方法中我们可以看出,实际include的类文件取决于下面这行代码1
$classFile = static::getAlias('@' . str_replace('\\', '/', $className) . '.php', false);
还是以 frontend\models\UserModel 为例,即1
$classFile = static::getAlias('@frontend/models/UserModel.php', false);
我们知道,在yii2中有一种别名的概念,比如这里的 @frontend。
所以,Yii::getAlias 方法的目的就是为了把路径别名 @frontend 转化为其被定义的统一路径。这也是别名存在的目的。
举一个活生生的例子,前面我们在实战rest的时候,在advanced基础上,新建了 api 应用,未定义 @api 别名名却直接 use api\models\UserModel 等类时就报错了,算是不少人的一种经历吧。
下面我们把主要目光转移到怎么把别名转化为路径上。
首先我们可以通过 Yii::setAlias 方法定义别名,定义的别名保存在 Yii::$aliases 属性上。
在入口文件中,require了这两个文件1
2require(__DIR__ . '/../../common/config/bootstrap.php');
require(__DIR__ . '/../config/bootstrap.php');
我们便可以在这两个文件内定义别名,比如 common/config/bootstrap.php内定义了如下别名1
2
3
4Yii::setAlias('@common', dirname(__DIR__));
Yii::setAlias('@frontend', dirname(dirname(__DIR__)) . '/frontend');
Yii::setAlias('@backend', dirname(dirname(__DIR__)) . '/backend');
Yii::setAlias('@console', dirname(dirname(__DIR__)) . '/console');
当然,你也可以在bootstrap.php文件内定义一些其他的全局性配置,比如大家在写接口时,就可以把接口地址定义在该文件内。
程序运行时,在需要使用别名的地方,比如 static::getAlias(‘@frontend/models/UserModel.php’, false) 这里,@frontend 就会被getAlias方法替换为在common/config/bootstrap.php 内用 Yii::setAlias方法定义的路径。
下面我们该看 Yii.php内的最后一行代码了1
Yii::$container = new yii\di\Container();
该行代码的作用是实例化一个全局的依赖注入容器。因为要讲的内容很多,所以我们下一节再讲吧。