这一部份主要讲的是 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 文件中定义。比如:
<?phpYii::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']);}// componentsforeach ($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 文件内容:
<?phprequire __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。
