应用的生命周期之预初始化

上文我们笼统的介绍了yii\web\Application类及其一系列父类之间的关系,希望各位把握好整体结构。

今天我们从上文尚未细说的 yii\web\Application::run 方法说起。

yii\web\Application类的run方法,实际指的是父类 yii\base\Application::run 方法,是启动整个应用程序的“钥匙”。

执行 yii\base\Application::run 方法之前, yii\base\Application 的构造方法construct 会先被执行,所以我们很有必要看一下这个构造方法 construct 。

1
2
3
4
5
6
7
8
9
10
11
12
13
public function __construct($config = [])
{
Yii::$app = $this;
static::setInstance($this);

$this->state = self::STATE_BEGIN;

$this->preInit($config);

$this->registerErrorHandler($config);

Component::__construct($config);
}

以上代码的含义,我们一句一句的分析:

Yii::$app = $this,yii\web\Application 对象被赋值给了 Yii::$app 属性,即 yii\baseYii 类的静态属性 $app,也就是说,程序运行时,我们在任意位置,都可以通过 Yii::$app 来引用 yii\web\Application 的实例了,比如我们可以通过 Yii::$app->id 来获取 yii\web\Application的id属性值,即应用的id

static::setInstance($this),调用yii\base\Module::setInstance方法,把当前module的实例保存到 yii\base\Application的loadedModules属性,这一点在 yii\base\Module::setInstance方法内有所体现

$this->state,通过 yii\base\Application的state属性,标记当前应用目前处于生命周期的哪个状态,一共有7个状态,7个状态值都是 yii\base\Application 的常量,你可以在 yii\base\Application类中看到7个以const定义的 STATE_ 打头的常量

$this->preInit($config), 调用 yii\base\Application::preInit 方法预初始化,$config 是我们4个配置文件通过 yii\helpers\ArrayHelper::merge处理的大数组,yii\base\Application::preInit 是我们这节重点分析的方法,来一起看一下

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public function preInit(&$config)
{
// 必须存在下标为id的单元,否则throw抛出异常
if (!isset($config['id'])) {
throw new InvalidConfigException('The "id" configuration for the Application is required.');
}
// 1、basePath 也是要必须设置的,其含义指的是当前module的root目录,不设置则抛出异常。我们看到basePath在 main.php中的值是 dirname(__DIR__),即 frontend 目录
// 2、调用 yii\base\Application::setBasePath 方法处理,会先调用 yii\base\Module::setBasePath 方法处理,yii\base\Module::setBasePath方法会获取 basePath的值的绝对路径,保存在 yii\base\Module::_basePath属性,表示当前module的root目录
// 3、yii\base\Application::setBasePath 跟着会通过 Yii::setAlias('@app', $this->getBasePath()) 设置别名 @app 指向 yii\base\Module::_basePath 所指目录,不信你可以在其他位置打印下 Yii::getAlias('@app') 看看结果
if (isset($config['basePath'])) {
$this->setBasePath($config['basePath']);
unset($config['basePath']);
} else {
throw new InvalidConfigException('The "basePath" configuration for the Application is required.');
}

// vendorPath,默认在common/config/main.php内配置的值是 dirname(dirname(__DIR__)) . '/vendor'
// 调用 yii\base\Application::setVendorPath 方法处理,其目的是设置三个别名,@vendor、@bower、@npm 你可以在 yii\base\Application::setVendorPath 方法内看到,也就是说composer的安装包的位置,我们也是可以通过配置指定的
if (isset($config['vendorPath'])) {
$this->setVendorPath($config['vendorPath']);
unset($config['vendorPath']);
} else {
// set "@vendor"
// 如果没有指定 vendorPath,默认vendorPath是 yii\base\Module::_basePath 上一级目录下的vendor目录
$this->getVendorPath();
}

// 如果配置了 runtimePath,即程序运行时的存储路径,比如日志路径,debug信息路径等都可以通过 runtimePath配置
// 同时,yii\base\Application::setRuntimePath 方法会设置 @runtime 别名指向该路径
if (isset($config['runtimePath'])) {
$this->setRuntimePath($config['runtimePath']);
unset($config['runtimePath']);
} else {
// set "@runtime"
// 如果没有配置runtimePath,默认的runtimePath是 yii\base\Module::_basePath 目录下的runtime目录,即@runtime默认指向这里
$this->getRuntimePath();
}

// 设置时区,我们尚未配置该选项,如果指定了,调用 yii\base\Application::setTimeZone会调用 date_default_timezone_set函数设置时区,否则将会以 php.ini内指定的时区为准,如果php.ini也未配置,默认时区格式是 UTC,也就是部分同学什么都没有配置时发现程序内的时间总是相差8小时的缘故
if (isset($config['timeZone'])) {
$this->setTimeZone($config['timeZone']);
unset($config['timeZone']);
} elseif (!ini_get('date.timezone')) {
$this->setTimeZone('UTC');
}

// 设置自己的container,当然,大多数情况下是没有必要的,默认的container是yii\di\Container
if (isset($config['container'])) {
$this->setContainer($config['container']);

unset($config['container']);
}

// merge core components with custom components
// 合并核心component和自定义的component
// 核心组件,在 yii\web\Application::coreComponents 和 yii\base\Application::coreComponents 方法内均有配置
// 自定义的component指的是 $config['components'] 内的配置
// 如果自定义的component跟核心component不冲突,则把核心component追加到 $config['components'];如果二者冲突且自定义的component结构规范,则以自定义的为准,规范指的是 是个数组且包含 class 下标
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'];
}
}
}

详细的介绍都在上面代码的注解中,不知道跟代码写在一起方便大家理解还是单独拎出来方便理解,有什么建议可以留言给我。

简单总结一下预初始化方法的目的:

检测配置是否正常,包括id,basePath必须配置项
设置module的root目录,并伴随一系列的别名设置,包括@app、@vendor、@bower、@npm 、@runtime
时区的设置
合并核心component和自定义的component
接着我们看 $this->registerErrorHandler($config),调用 yii\base\Application::registerErrorHandler 方法,把 errorHandler 组件注册为应用的错误处理程序,即应用内php程序报错时,都通过 errorHandler 组件处理。

具体怎么实现的,可能我们要单独拎出来一个“分支”来说。

最后,我们看 Component::__construct($config),从上文我们了解到,yii\base\Application父类的父类的父类是 yii\base\Component,有同学不明白这里为什么要这样调用。

通常,我们调用父类的构造方法可以使用 parent::construct,但是没有 ppparent::construct方法呀(ppparent调侃为父父父类),所以这里只能使用 Component::__construct的形式来调用。

调用 Component::construct方法的作用是什么呢?Component::construct实际指的是 yii\base\Object::__construct。

我们来看一下 Object::__construct 方法,下面的实现很关键。

1
2
3
4
5
6
7
public function __construct($config = [])
{
if (!empty($config)) {
Yii::configure($this, $config);
}
$this->init();
}

可以看到,该方法有两个重点含义

调用 yii\BaseYii::configure 方法,初始化 yii\web\Application 类的属性
调用 yii\web\Application::init 方法完成初始化操作
以上两点,我们来分别解释一下

首先 Object::__construct方法中,Yii::configure($this, $config) 中,$this 自然指的是 yii\web\Application,$config 指的是我们的配置数组。

我们看一下 yii\BaseYii::configure方法

1
2
3
4
5
6
7
8
public static function configure($object, $properties)
{
foreach ($properties as $name => $value) {
$object->$name = $value;
}

return $object;
}

从该方法的实现上可以看出,Object::__construct方法中的 Yii::configure($this, $config) ,即是循环配置数组,以数组的key为 yii\web\Application 的属性,值为对应的属性值。这也是我们上文说$config不仅仅是个简单的数组的实现。

那有同学要说了,万一我在main.php中添加了一个不是 yii\web\Application类的属性,是不是要报错?

这不一定。举例来说,components 项就不是 yii\web\Application 类的属性,再者,我们以前在配置文件中添加的行为 ‘as xxx’ ,是不是都没有报错?

这是如何实现的呢?按理说应该报错的呀。

具体实现我们准备后面再谈一谈。

提示:魔术方法__set?

第二点,yii\base\Object::construct 方法内执行 $this->init(),也就是说,但凡是继承 yii\base\Object 的类,init方法都是在 construct 方法运行后调用。这在多继承复杂的类时,很重要也很方便。前提是一旦重写父类的 construct 方法,记得调用 parent::construct 哦。

这里 $this->init 方法的调用,实际是调用 yii\base\Application::init 方法。该方法则会带入我们进入下一阶段:“应用的初始化”。

但是,我们并不着急开讲“应用的初始化”,这一节我们预留了两个“后面再谈”,其背后的实现——Component以及ServiceLocator,这很重要。

下一节我们脱离一下主线,再开一个“分支”,来搞清楚底层对应用属性和component的实现。