这一部份主要讲的是 Yii 中以约定的方式来实现的功能,或者说是惯用的模式。仅是推荐性、建议性的,而并非是强制性。不建议随意更改 Yii 设定的约定。
Yii 的约定内容,主要包含应用的目录结构、别名、自动加载机制、环境、对象配置等内容。
1. Yii应用的目录结构和入口脚本
高级模版安装后典型的 Yii 应用的目录结构:
-- backend
-- common
-- console
-- environments
-- frontend
-- nbproject
-- tests
-- vendor
-- composer.json
-- composer.lock
-- init
-- init.bat
-- LICENSE.md
-- README.md
-- requirements.php
-- yii
-- yii.bat
对于高级应用而言,相当于有 backend frontend console 三个独立的 Yii 应用。
1.1 公共目录
2. 别名(Alias)
可以将别名视为特殊的常量变量,他的作用在于避免将一些文件路径、URL 以硬编码的方式写入代码中,或者多处出现一长串的文件路径、URL。
2.1 预定义的别名
Yii 中别名以 ‘@’ 开头,以区别于正常的文件路径和 URL。Yii 中预定义了许多常用的别名。别名的定义一般放在应用的最开始的阶段进行,比如引导阶段、初始化阶段等,可以保证后续代码可以使用这些定义好的别名。
2.1.1 配置文件中的别名
别名一般放在 common\config\bootstrap.php ,或者 frontend\config\bootstrap.php 等 bootstrap.php
文件中定义。比如:
<?php
Yii::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 文件定义了 @common ,@frontend ,@backend 和 @console 4 个别名。开发者也可以自己在 bootstrap.php 中加入自己的别名定义,这是最常运用的定义别名的方式。
2.1.2 Yii 预定义的别名
相比较于通过 bootstrap.php 的方式定义别名,有的别名就不那么直观了,与 Yii 框架和应用自身息息相关。这类别名直接写到 Yii 的代码中去了。这些预定义的别名,主要分布在 yii\BaseYii
和 yii\base\Application
等类中。
在 yii\BaseYii 中:
<?
// 定义了 @yii 别名
public static $aliases = ['@yii' => __DIR__];
在 yii\base\Application 在其构造函数 __construct() 中,会调用 preInit($config)
,具体如下:
<?
public function preInit(&$config) {
if (!isset($config['id'])) {
throw new InvalidConfigException(
'The "id" configuration for the Application is required.');
}
// basePath 必须在配置文件中给出,否则会抛出弃常
if (isset($config['basePath'])) {
// 这里会设置 @app
$this->setBasePath($config['basePath']);
unset($config['basePath']);
} else {
throw new InvalidConfigException(
'The "basePath" configuration for the Application is required.');
}
// @vendor 如果配置文件中设置了 vendorPath 使用配置的值,否则使用默认的
if (isset($config['vendorPath'])) {
$this->setVendorPath($config['vendorPath']);
unset($config['vendorPath']);
} else {
// set "@vendor"
$this->getVendorPath();
}
// @runtime 如果配置文件中设置了 runtimePath ,就使用配置的值,否则使用默认的
if (isset($config['runtimePath'])) {
$this->setRuntimePath($config['runtimePath']);
unset($config['runtimePath']);
} else {
$this->getRuntimePath();
}
// 时区
if (isset($config['timeZone'])) {
$this->setTimeZone($config['timeZone']);
unset($config['timeZone']);
} elseif (!ini_get('date.timezone')) {
$this->setTimeZone('UTC');
}
// DI 容器
if (isset($config['container'])) {
$this->setContainer($config['container']);
unset($config['container']);
}
// components
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'];
}
}
}
在定义 basePath 时,Yii 顺便定义了 @app
,代码在 yii\base\Application::setBasePath()
中:
<?
public function setBasePath($path) {
parent::setBasePath($path);
Yii::setAlias('@app', $this->getBasePath());
}
vendor 目录并不在 frontend 或 backend 目录下,而是跟他们是兄弟目录。这是因为对于整个工程而言,这个 vendor 的内容是 frontend 和 backend 等共用的。
2.2 定义与解析别名
Yii::setAlias()
是定义别名的关键。实际上, 该方法的代码在 BaseYii::setAlias()
中:
<?
public static function setAlias($alias, $path) {
// 如果拟定义的别名并非以 @ 打头,则在前面加上 @
if (strncmp($alias, '@', 1)) {
$alias = '@' . $alias;
}
// 找到别名的第一段,即 @ 到第一个 / 之间的内容,如 @foo/bar/qux 的 @foo
$pos = strpos($alias, '/');
$root = $pos === false ? $alias : substr($alias, 0, $pos);
if ($path !== null) {
// 去除路径末尾的 \ / 。如果路径本身就是一个别名,直接解析出来
$path = strncmp($path, '@', 1) ? rtrim($path, '\\/') : static::getAlias($path);
// 检查是否有 $aliases[$root],
// 看看是否已经定义好了根别名。如果没有,则以 $root 为键,保存这个别名
if (!isset(static::$aliases[$root])) {
if ($pos === false) {
static::$aliases[$root] = $path;
} else {
static::$aliases[$root] = [$alias => $path];
}
// 如果 $aliases[$root] 已经存在,则替换成新的路径,或增加新的路径
} elseif (is_string(static::$aliases[$root])) {
if ($pos === false) {
static::$aliases[$root] = $path;
} else {
static::$aliases[$root] = [
$alias => $path,
$root => static::$aliases[$root],
];
}
} else {
static::$aliases[$root][$alias] = $path;
krsort(static::$aliases[$root]);
}
// 当传入的 $path 为 null 时,表示要删除这个别名。
} elseif (isset(static::$aliases[$root])) {
if (is_array(static::$aliases[$root])) {
unset(static::$aliases[$root][$alias]);
} elseif ($pos === false) {
unset(static::$aliases[$root]);
}
}
}
- 别名规范化 如果要定义的别名 $alias 并非以 @ 打头,自动为这个别名加上 @ 前缀。
获取根别名 $alias 的根别名,就是 @ 加上第一个 / 之间地内容,以 $root 表示。这里可以看出,别名是分 层次的。下面 3 个语句的根别名都是 @foo:
<?
Yii::setAlias('@foo', 'path/to/some/where');
Yii::setAlias('@foo/bar', 'path/to/some/where');
Yii::setAlias('@foo/bar/qux', 'path/to/some/where');
新定义别名还是删除别名 如果传入的 $path 不是 null ,说明是正常的别名定义。对于正常的别名定义,就 是往 BaseYii::$aliases[] 里写入信息。而如果 $path 为 null ,说明是要删除别名: ```php <? // 定义别名 @foo Yii::setAlias(‘@foo’, ‘path/to/some/where’);
// 删除别名 @foo Yii::setAlias(‘@foo’, null);
- **解析** $path 对于新定义别名,既然 $path 不为 null ,那么先进行解析:如果 $path 以 @ 打头,说明这也是一个别名,则调用 Yii::getAlias() ,并将解析后的结果作为新的 $path ;如果 $path 不以 @ 打头,说明是一个正常的 path 或 URL,那么去除 $path 末尾的 / 和 \ 。
- **别名的写入** 对于全新的别名,也即其根别名是新的,BaseYii::aliases[$root] 不存在。那么全新别名的写入分两种情况:
- 如果全新别名本身就是根别名,那么直接 BaseYii::aliases[$alias] = $path ;
- 而如果全新的别名并非是一个根别名,即形如 @foo/bar 带有二级、三级等路径的,BaseYii::aliases[$root] = [$alias => $path] 。
```php
<?
// BaseYii::aliases['@foo']=['@foo/bar' => 'path/to/foo/bar']
Yii::setAlias('@foo/bar', 'path/to/foo/bar');
// BaseYii::aliases['@qux'] = 'path/to/qux'
Yii::setAlias('@qux', 'path/to/qux');
2.2.1 别名的解析过程
与定义过程使用 Yii::setAlias() 相对应,别名的解析过程使用 Yii::getAlias()
,实际代码在 BaseYii::getAlias()
中:
<?
public static function getAlias($alias, $throwException = true) {
// 一切不以 @ 打头的别名都是无效的
if (strncmp($alias, '@', 1)) {
return $alias;
}
// 先确定根别名 $root
$pos = strpos($alias, '/');
$root = $pos === false ? $alias : substr($alias, 0, $pos);
// 从根别名开始找起,如果根别名没找到,一切免谈
if (isset(static::$aliases[$root])) {
if (is_string(static::$aliases[$root])) {
return $pos === false ? static::$aliases[$root] : static::$aliases[$root] . substr($alias, $pos);
}
foreach (static::$aliases[$root] as $name => $path) {
if (strpos($alias . '/', $name . '/') === 0) {
return $path . substr($alias, strlen($name));
}
}
}
if ($throwException) {
throw new InvalidArgumentException("Invalid path alias: $alias");
}
return false;
}
3. Yii的类自动加载机制
3.1 自动加载机制的实现
Yii 的类自动加载,依赖于 PHP 的 spl_autoload_register() ,注册一个自己的自动加载函数(autoloader),并插入到自动加载函数栈的最前面,确保 Yii 的 autoloader 会被最先调用。
类自动加载的这个机制的引入要从入口文件 index.php 开始说起:
<?
// 注册第三方自动加载
require __DIR__ . '/../../vendor/autoload.php';
// 这个是 Yii 的 Autoloader,放在最后面,确保其插入的 autoloader 会放在最前面
require __DIR__ . '/../../vendor/yiisoft/yii2/Yii.php';
// 后面不应再有 autoloader 了
这个文件重点在于第三方 autoloader 与 Yii 实现的 autoloader 的顺序。不管第三方的代码是如何使用 spl_autoload_register() 来注册自己的 autoloader 的,只要 Yii 的代码在最后面,就可以确保其可以将自己的 autoloader 插入到整个 autoloder 栈的最前面,从而在需要时最先被调用。
接下来,看看 Yii 是如何调用 spl_autoload_register() 注册 autoloader 的, Yii.php
文件内容:
<?php
require __DIR__ . '/BaseYii.php';
class Yii extends \yii\BaseYii {
}
spl_autoload_register(['Yii', 'autoload'], true, true);
// ...
调用了 spl_autoload_register([‘Yii’, ‘autoload’, true, true]) ,将 Yii::autoload() 作为 autoloader 插入到栈的最前面。并将 classes.php 读取到 Yii::$classMap 中,保存了一个映射表,这个映射表,保存了一系列的类名与其所在 PHP 文件的映射关系比如:
<?
return [
'yii\base\Action' => YII2_PATH . '/base/Action.php',
'yii\base\ActionEvent' => YII2_PATH . '/base/ActionEvent.php',
...
'yii\widgets\PjaxAsset' => YII2_PATH . '/widgets/PjaxAsset.php',
'yii\widgets\Spaceless' => YII2_PATH . '/widgets/Spaceless.php',
];
这个映射表以类名为键,以实际类文件为值,Yii 所有的核心类都已经写入到这个 classes.php 文件中,所以,核心类的加载是最便捷,最快的。现在,来看看 BaseYii::autoload():
<?
public 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;
if (YII_DEBUG && !class_exists($className, false)
&& !interface_exists($className, false) && !trait_exists($className, false)) {
throw new UnknownClassException(
"Unable to find '$className' in file: $classFile. Namespace missing?");
}
}
运作原理:
- 检查 $classMap[$className] 看看是否在映射表中已经有拟加载类的位置信息;
- 如果有,再看看这个位置信息是不是一个路径别名,即是不是以 ‘@’ 打头,是的话,将路径别名解析成实际路径。如果映射表中的位置信息并非一个路径别名,那么将这个路径作为类文件的所在位置。类文件的完整路径保存在
$classFile
; - 如果 $classMap[$className] 没有该类的信息,那么,看看这个类名中是否含有 \ ,如果没有,说明这是一个不符合规范要求的类名,autoloader 直接返回。PHP 会尝试使用其他已经注册的 autoloader 进行加载。如果有 \ ,认为这个类名符合规范,将其转换成路径形式。即所有的 \ 用 / 替换,并加上 .php 的后缀。
- 将替换后的类名,加上 @ 前缀,作为一个路径别名,进行解析。从别名的解析过程我们知道,如果根别名不存在,将会抛出异常。所以,类的命名,必须以有效的根别名打头: ```php <? // 有效的类名,因为 @yii 是一个已经预定义好的别名 use yii\base\Application;
// 无效的类名,因为没有 @foo 或 @foo/bar 的根别名,要提前定义好 use foo\bar\SomeClass;
- 使用 PHP 的 include() 将类文件加载进来,实现类的加载。
从其运作原理看,最快找到类的方式是使用映射表。其次,Yii 中所有的类名,除了符合规范外,还需要提前注册有效的根别名。
<a name="lacTV"></a>
## 3.2 运用自动加载机制
在入口脚本中,除了 Yii 自己的 autoloader,还有一个第三方的 autoloader:
```php
<?
require(__DIR__ . '/../../vendor/autoload.php');
这个其实是 Composer
提供的 autoloader。Yii 使用 Composer 来作为包依赖管理器,因此,建议保留Composer 的 autoloader,尽管 Yii 的 autoloader 也能自动加载使用 Composer 安装的第三方库、扩展等,而且更为高效。但考虑到毕竟是 Composer 安装的,其有一套自己专门的规则,从维护性、兼容性、扩展性来考虑,建议保留 Composer 的 autoloader。