什么是类的自动加载,详见PHP官方文档 http://php.net/manual/zh/language.oop5.autoload.php
Yii的类自动加载,依赖于PHP的 spl_autoload_register()
, 注册一个自己的自动加载函数(autoloader
),类仅在调用时才会被加载,依赖别名实现了快速定位,这也是YII高性能的一个重要体现。
首先,从入口文件开始分析。
php
// comment out the following two lines when deployed to production
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');
require __DIR__ . '/../vendor/autoload.php';//引入composer的自动加载
require __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';//引入YII
$config = require __DIR__ . '/../config/web.php';//引入配置文件
(new yii\web\Application($config))->run();
我们直接来看Yii.php
require __DIR__ . '/BaseYii.php';
class Yii extends \yii\BaseYii
{
}
spl_autoload_register(['Yii', 'autoload'], true, true);//注册YII的自动加载函数
Yii::$classMap = require __DIR__ . '/classes.php';//导入classMap(一个类的映射表)
Yii::$container = new yii\di\Container();
因为用的是spl_autoload_register(['Yii', 'autoload'], true, true)
,尽管前面先引入的composer的自动加载,当第三个参数为true时,会将该自动加载放到队首,所以最先使用。
因为Yii.php继承了BaseYii,我们到BaseYii中看autoload方法
public static function autoload($className)
{
if (isset(static::$classMap[$className])) {//如果在上面代码引入的classmap中,则直接读取
$classFile = static::$classMap[$className];
if ($classFile[0] === '@') {
$classFile = static::getAlias($classFile);//如果是个别名,就getAlias
}
} elseif (strpos($className, '\\') !== false) {//如果类名中含有\则认为是一个正确的类名,将所有的\转换成/并在前面加上@在后面加上.php,作为一个路径别名进行解析,然后执行getAlias方法,得到实际的路径
$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?");
}
}
看到这,就必须得看一下别名了
别名用来表示文件路径和 URL,这样就避免了在代码中硬编码一些绝对路径和 URL。 一个别名必须以 @ 字符开头,以区别于传统的文件路径和 URL。 Yii 预定义了大量可用的别名。例如,别名 @yii 指的是 Yii 框架本身的安装目录,而 @web 表示的是当前运行应用的根 URL。
注意:别名所指向的文件路径或 URL 不一定是真实存在的文件或资源。
在\yii\base\Application
中
public function __construct($config = [])
{
Yii::$app = $this;//将application对象赋值给Yii::$app,可以直接用Yii::$app调用application的方法
static::setInstance($this);
$this->state = self::STATE_BEGIN;
$this->preInit($config);//初始化一些配置参数和加载核心组件
$this->registerErrorHandler($config);//注册错误处理函数
Component::__construct($config);//注册一些其他的组件和配置
}
public function preInit(&$config)
{
if (!isset($config['id'])) {
throw new InvalidConfigException('The "id" configuration for the Application is required.');
}
if (isset($config['basePath'])) {
$this->setBasePath($config['basePath']);//设置@app别名为根目录,这里正是为什么命名空间都是app\开头的原因
unset($config['basePath']);
} else {
throw new InvalidConfigException('The "basePath" configuration for the Application is required.');
}
if (isset($config['vendorPath'])) {
$this->setVendorPath($config['vendorPath']);//设置@vendor,@bower,@npm别名
unset($config['vendorPath']);
} else {
// set "@vendor"
$this->getVendorPath();
}
...
// merge core components with custom 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'];
}
}
}
public function getVendorPath()
{
// 在未设置vendorPath时,使用默认值
if ($this->_vendorPath === null) {
$this->setVendorPath($this->getBasePath() . DIRECTORY_SEPARATOR . 'vendor');
}
return $this->_vendorPath;
}
// 这里定义了3个别名
public function setVendorPath($path)
{
$this->_vendorPath = Yii::getAlias($path);
Yii::setAlias('@vendor', $this->_vendorPath);
Yii::setAlias('@bower', $this->_vendorPath . DIRECTORY_SEPARATOR . 'bower');
Yii::setAlias('@npm', $this->_vendorPath . DIRECTORY_SEPARATOR . 'npm');
}
public function getRuntimePath()
{
// 在未设置runtimePath时,使用默认值
if ($this->_runtimePath === null) {
$this->setRuntimePath($this->getBasePath() . DIRECTORY_SEPARATOR . 'runtime');
}
return $this->_runtimePath;
}
// 这里定义了 @runtime 别名
public function setRuntimePath($path)
{
$this->_runtimePath = Yii::getAlias($path);
Yii::setAlias('@runtime', $this->_runtimePath);
}
接下来咱们看一下设置别名和获取别名的方法
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]);
}
}
}
根据上面的方法,可以看到,yii支持给别名再设置一个别名,比如
// 使用一个别名定义另一个别名
Yii::setAlias('@fooqux', '@foo/qux');
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);
} else {
// 由于写入前使用了 krsort() 所以,较长的别名会被先遍历到。
foreach (static::$aliases[$root] as $name => $path) {
if (strpos($alias . '/', $name . '/') === 0) {
return $path . substr($alias, strlen($name));
}
}
}
}
if ($throwException) {
throw new InvalidParamException("Invalid path alias: $alias");
} else {
return false;
}
}
别名的解析过程相对简单:
YII框架的预定义别名如下
array(8) {
["@yii"]=>
array(6) {
["@yii/swiftmailer"]=>
string(63) "basic/vendor/yiisoft/yii2-swiftmailer"
["@yii/gii"]=>
string(55) "basic/vendor/yiisoft/yii2-gii"
["@yii/faker"]=>
string(57) "basic/vendor/yiisoft/yii2-faker"
["@yii/debug"]=>
string(57) "basic/vendor/yiisoft/yii2-debug"
["@yii/bootstrap"]=>
string(61) "basic/vendor/yiisoft/yii2-bootstrap"
["@yii"]=>
string(51) "basic/vendor/yiisoft/yii2"
}
["@app"]=>
string(31) "basic"
["@vendor"]=>
string(38) "basic/vendor"
["@bower"]=>
string(50) "basic/vendor/bower-asset"
["@npm"]=>
string(48) "basic/vendor/npm-asset"
["@runtime"]=>
string(39) "basic/runtime"
["@webroot"]=>
string(1) "."
["@web"]=>
string(1) "."
}
Yii的自动加载,就是结合类的映射表+命名空间别名的形式进行快速定位和载入的,本文只是简单的罗列了一下代码,关于更深层次的东西,可以自己根据编辑器代码跟踪功能一步步查看。