这一部份主要讲的是 Yii 中以约定的方式来实现的功能,或者说是惯用的模式。仅是推荐性、建议性的,而并非是强制性。不建议随意更改 Yii 设定的约定。
Yii 的约定内容,主要包含应用的目录结构、别名、自动加载机制、环境、对象配置等内容。

1. Yii应用的目录结构和入口脚本

高级模版安装后典型的 Yii 应用的目录结构:

  1. -- backend
  2. -- common
  3. -- console
  4. -- environments
  5. -- frontend
  6. -- nbproject
  7. -- tests
  8. -- vendor
  9. -- composer.json
  10. -- composer.lock
  11. -- init
  12. -- init.bat
  13. -- LICENSE.md
  14. -- README.md
  15. -- requirements.php
  16. -- yii
  17. -- 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 文件中定义。比如:

  1. <?php
  2. Yii::setAlias('common', dirname(__DIR__));
  3. Yii::setAlias('frontend', dirname(dirname(__DIR__)) . '/frontend');
  4. Yii::setAlias('backend', dirname(dirname(__DIR__)) . '/backend');
  5. 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\BaseYiiyii\base\Application 等类中。
在 yii\BaseYii 中:

  1. <?
  2. // 定义了 @yii 别名
  3. public static $aliases = ['@yii' => __DIR__];

在 yii\base\Application 在其构造函数 __construct() 中,会调用 preInit($config) ,具体如下:

  1. <?
  2. public function preInit(&$config) {
  3. if (!isset($config['id'])) {
  4. throw new InvalidConfigException(
  5. 'The "id" configuration for the Application is required.');
  6. }
  7. // basePath 必须在配置文件中给出,否则会抛出弃常
  8. if (isset($config['basePath'])) {
  9. // 这里会设置 @app
  10. $this->setBasePath($config['basePath']);
  11. unset($config['basePath']);
  12. } else {
  13. throw new InvalidConfigException(
  14. 'The "basePath" configuration for the Application is required.');
  15. }
  16. // @vendor 如果配置文件中设置了 vendorPath 使用配置的值,否则使用默认的
  17. if (isset($config['vendorPath'])) {
  18. $this->setVendorPath($config['vendorPath']);
  19. unset($config['vendorPath']);
  20. } else {
  21. // set "@vendor"
  22. $this->getVendorPath();
  23. }
  24. // @runtime 如果配置文件中设置了 runtimePath ,就使用配置的值,否则使用默认的
  25. if (isset($config['runtimePath'])) {
  26. $this->setRuntimePath($config['runtimePath']);
  27. unset($config['runtimePath']);
  28. } else {
  29. $this->getRuntimePath();
  30. }
  31. // 时区
  32. if (isset($config['timeZone'])) {
  33. $this->setTimeZone($config['timeZone']);
  34. unset($config['timeZone']);
  35. } elseif (!ini_get('date.timezone')) {
  36. $this->setTimeZone('UTC');
  37. }
  38. // DI 容器
  39. if (isset($config['container'])) {
  40. $this->setContainer($config['container']);
  41. unset($config['container']);
  42. }
  43. // components
  44. foreach ($this->coreComponents() as $id => $component) {
  45. if (!isset($config['components'][$id])) {
  46. $config['components'][$id] = $component;
  47. } elseif (is_array($config['components'][$id]) &&
  48. !isset($config['components'][$id]['class'])) {
  49. $config['components'][$id]['class'] = $component['class'];
  50. }
  51. }
  52. }

在定义 basePath 时,Yii 顺便定义了 @app ,代码在 yii\base\Application::setBasePath() 中:

  1. <?
  2. public function setBasePath($path) {
  3. parent::setBasePath($path);
  4. Yii::setAlias('@app', $this->getBasePath());
  5. }

vendor 目录并不在 frontend 或 backend 目录下,而是跟他们是兄弟目录。这是因为对于整个工程而言,这个 vendor 的内容是 frontend 和 backend 等共用的。

2.2 定义与解析别名

Yii::setAlias() 是定义别名的关键。实际上, 该方法的代码在 BaseYii::setAlias() 中:

  1. <?
  2. public static function setAlias($alias, $path) {
  3. // 如果拟定义的别名并非以 @ 打头,则在前面加上 @
  4. if (strncmp($alias, '@', 1)) {
  5. $alias = '@' . $alias;
  6. }
  7. // 找到别名的第一段,即 @ 到第一个 / 之间的内容,如 @foo/bar/qux 的 @foo
  8. $pos = strpos($alias, '/');
  9. $root = $pos === false ? $alias : substr($alias, 0, $pos);
  10. if ($path !== null) {
  11. // 去除路径末尾的 \ / 。如果路径本身就是一个别名,直接解析出来
  12. $path = strncmp($path, '@', 1) ? rtrim($path, '\\/') : static::getAlias($path);
  13. // 检查是否有 $aliases[$root],
  14. // 看看是否已经定义好了根别名。如果没有,则以 $root 为键,保存这个别名
  15. if (!isset(static::$aliases[$root])) {
  16. if ($pos === false) {
  17. static::$aliases[$root] = $path;
  18. } else {
  19. static::$aliases[$root] = [$alias => $path];
  20. }
  21. // 如果 $aliases[$root] 已经存在,则替换成新的路径,或增加新的路径
  22. } elseif (is_string(static::$aliases[$root])) {
  23. if ($pos === false) {
  24. static::$aliases[$root] = $path;
  25. } else {
  26. static::$aliases[$root] = [
  27. $alias => $path,
  28. $root => static::$aliases[$root],
  29. ];
  30. }
  31. } else {
  32. static::$aliases[$root][$alias] = $path;
  33. krsort(static::$aliases[$root]);
  34. }
  35. // 当传入的 $path 为 null 时,表示要删除这个别名。
  36. } elseif (isset(static::$aliases[$root])) {
  37. if (is_array(static::$aliases[$root])) {
  38. unset(static::$aliases[$root][$alias]);
  39. } elseif ($pos === false) {
  40. unset(static::$aliases[$root]);
  41. }
  42. }
  43. }
  • 别名规范化 如果要定义的别名 $alias 并非以 @ 打头,自动为这个别名加上 @ 前缀。
  • 获取根别名 $alias 的根别名,就是 @ 加上第一个 / 之间地内容,以 $root 表示。这里可以看出,别名是分 层次的。下面 3 个语句的根别名都是 @foo:

    1. <?
    2. Yii::setAlias('@foo', 'path/to/some/where');
    3. Yii::setAlias('@foo/bar', 'path/to/some/where');
    4. 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);

  1. - **解析** $path 对于新定义别名,既然 $path 不为 null ,那么先进行解析:如果 $path @ 打头,说明这也是一个别名,则调用 Yii::getAlias() ,并将解析后的结果作为新的 $path ;如果 $path 不以 @ 打头,说明是一个正常的 path URL,那么去除 $path 末尾的 / \
  2. - **别名的写入** 对于全新的别名,也即其根别名是新的,BaseYii::aliases[$root] 不存在。那么全新别名的写入分两种情况:
  3. - 如果全新别名本身就是根别名,那么直接 BaseYii::aliases[$alias] = $path
  4. - 而如果全新的别名并非是一个根别名,即形如 @foo/bar 带有二级、三级等路径的,BaseYii::aliases[$root] = [$alias => $path]
  5. ```php
  6. <?
  7. // BaseYii::aliases['@foo']=['@foo/bar' => 'path/to/foo/bar']
  8. Yii::setAlias('@foo/bar', 'path/to/foo/bar');
  9. // BaseYii::aliases['@qux'] = 'path/to/qux'
  10. Yii::setAlias('@qux', 'path/to/qux');

2.2.1 别名的解析过程

与定义过程使用 Yii::setAlias() 相对应,别名的解析过程使用 Yii::getAlias() ,实际代码在 BaseYii::getAlias() 中:

  1. <?
  2. public static function getAlias($alias, $throwException = true) {
  3. // 一切不以 @ 打头的别名都是无效的
  4. if (strncmp($alias, '@', 1)) {
  5. return $alias;
  6. }
  7. // 先确定根别名 $root
  8. $pos = strpos($alias, '/');
  9. $root = $pos === false ? $alias : substr($alias, 0, $pos);
  10. // 从根别名开始找起,如果根别名没找到,一切免谈
  11. if (isset(static::$aliases[$root])) {
  12. if (is_string(static::$aliases[$root])) {
  13. return $pos === false ? static::$aliases[$root] : static::$aliases[$root] . substr($alias, $pos);
  14. }
  15. foreach (static::$aliases[$root] as $name => $path) {
  16. if (strpos($alias . '/', $name . '/') === 0) {
  17. return $path . substr($alias, strlen($name));
  18. }
  19. }
  20. }
  21. if ($throwException) {
  22. throw new InvalidArgumentException("Invalid path alias: $alias");
  23. }
  24. return false;
  25. }

3. Yii的类自动加载机制

3.1 自动加载机制的实现

Yii 的类自动加载,依赖于 PHP 的 spl_autoload_register() ,注册一个自己的自动加载函数(autoloader),并插入到自动加载函数栈的最前面,确保 Yii 的 autoloader 会被最先调用。
类自动加载的这个机制的引入要从入口文件 index.php 开始说起:

  1. <?
  2. // 注册第三方自动加载
  3. require __DIR__ . '/../../vendor/autoload.php';
  4. // 这个是 Yii 的 Autoloader,放在最后面,确保其插入的 autoloader 会放在最前面
  5. require __DIR__ . '/../../vendor/yiisoft/yii2/Yii.php';
  6. // 后面不应再有 autoloader 了

这个文件重点在于第三方 autoloader 与 Yii 实现的 autoloader 的顺序。不管第三方的代码是如何使用 spl_autoload_register() 来注册自己的 autoloader 的,只要 Yii 的代码在最后面,就可以确保其可以将自己的 autoloader 插入到整个 autoloder 栈的最前面,从而在需要时最先被调用。
接下来,看看 Yii 是如何调用 spl_autoload_register() 注册 autoloader 的, Yii.php 文件内容:

  1. <?php
  2. require __DIR__ . '/BaseYii.php';
  3. class Yii extends \yii\BaseYii {
  4. }
  5. spl_autoload_register(['Yii', 'autoload'], true, true);
  6. // ...

调用了 spl_autoload_register([‘Yii’, ‘autoload’, true, true]) ,将 Yii::autoload() 作为 autoloader 插入到栈的最前面。并将 classes.php 读取到 Yii::$classMap 中,保存了一个映射表,这个映射表,保存了一系列的类名与其所在 PHP 文件的映射关系比如:

  1. <?
  2. return [
  3. 'yii\base\Action' => YII2_PATH . '/base/Action.php',
  4. 'yii\base\ActionEvent' => YII2_PATH . '/base/ActionEvent.php',
  5. ...
  6. 'yii\widgets\PjaxAsset' => YII2_PATH . '/widgets/PjaxAsset.php',
  7. 'yii\widgets\Spaceless' => YII2_PATH . '/widgets/Spaceless.php',
  8. ];

这个映射表以类名为键,以实际类文件为值,Yii 所有的核心类都已经写入到这个 classes.php 文件中,所以,核心类的加载是最便捷,最快的。现在,来看看 BaseYii::autoload():

  1. <?
  2. public static function autoload($className) {
  3. if (isset(static::$classMap[$className])) {
  4. $classFile = static::$classMap[$className];
  5. if ($classFile[0] === '@') {
  6. $classFile = static::getAlias($classFile);
  7. }
  8. } elseif (strpos($className, '\\') !== false) {
  9. $classFile = static::getAlias('@' . str_replace('\\', '/', $className) . '.php', false);
  10. if ($classFile === false || !is_file($classFile)) {
  11. return;
  12. }
  13. } else {
  14. return;
  15. }
  16. include $classFile;
  17. if (YII_DEBUG && !class_exists($className, false)
  18. && !interface_exists($className, false) && !trait_exists($className, false)) {
  19. throw new UnknownClassException(
  20. "Unable to find '$className' in file: $classFile. Namespace missing?");
  21. }
  22. }

运作原理:

  • 检查 $classMap[$className] 看看是否在映射表中已经有拟加载类的位置信息;
  • 如果有,再看看这个位置信息是不是一个路径别名,即是不是以 ‘@’ 打头,是的话,将路径别名解析成实际路径。如果映射表中的位置信息并非一个路径别名,那么将这个路径作为类文件的所在位置。类文件的完整路径保存在 $classFile
  • 如果 $classMap[$className] 没有该类的信息,那么,看看这个类名中是否含有 \ ,如果没有,说明这是一个不符合规范要求的类名,autoloader 直接返回。PHP 会尝试使用其他已经注册的 autoloader 进行加载。如果有 \ ,认为这个类名符合规范,将其转换成路径形式。即所有的 \ 用 / 替换,并加上 .php 的后缀。
  • 将替换后的类名,加上 @ 前缀,作为一个路径别名,进行解析。从别名的解析过程我们知道,如果根别名不存在,将会抛出异常。所以,类的命名,必须以有效的根别名打头: ```php <? // 有效的类名,因为 @yii 是一个已经预定义好的别名 use yii\base\Application;

// 无效的类名,因为没有 @foo 或 @foo/bar 的根别名,要提前定义好 use foo\bar\SomeClass;

  1. - 使用 PHP include() 将类文件加载进来,实现类的加载。
  2. 从其运作原理看,最快找到类的方式是使用映射表。其次,Yii 中所有的类名,除了符合规范外,还需要提前注册有效的根别名。
  3. <a name="lacTV"></a>
  4. ## 3.2 运用自动加载机制
  5. 在入口脚本中,除了 Yii 自己的 autoloader,还有一个第三方的 autoloader
  6. ```php
  7. <?
  8. require(__DIR__ . '/../../vendor/autoload.php');

这个其实是 Composer 提供的 autoloader。Yii 使用 Composer 来作为包依赖管理器,因此,建议保留Composer 的 autoloader,尽管 Yii 的 autoloader 也能自动加载使用 Composer 安装的第三方库、扩展等,而且更为高效。但考虑到毕竟是 Composer 安装的,其有一套自己专门的规则,从维护性、兼容性、扩展性来考虑,建议保留 Composer 的 autoloader。

4. 环境和配置文件

5. 配置项(Configuration)