本文分为两部分:第一部分讲__autoload()函数实现的类自动加载。第二部分讲spl_autoload_register()函数实现的类自动加载。第三部分讲spl_autoload_register()相比较__autoload()的好处。第四部分讲yii的autoload机制。
第一部分: _autoload()函数实现的类自动加载
在面向对象编程中,都是以对象为单位的操作,如果我有两个不同的类,类A和类B,在同一个文件里,实例化对象,就能在这个文件同时调用类A和类B的函数
<?php
#a.php
class A{
public function funA()
{
echo 'The class:'.__class__.'<br/>';
}
}
class B{
public function funB()
{
echo 'The class:'.__class__.'<br/>';
}
}
$a = new A();
$b = new B();
$a->funA(); //The class:A
$b->funB(); //The class:B
两个类都在同一个文件,所有能运行成功,但现在大部分应用都分MVC,不同类专门处理特定的事物,比如C(Controller),只执行与事物有关的逻辑操作,这些文件很明显分属于不同目录下:
|—root
|—controller
|—controllerA.php
|—model
|—modelA.php
|—view
|—viewA.php
这时我要在modelA.php里怎么引用controllerA.php的函数呢?这就用到__autoload()了。
<?php
#保存在MyClass.php
class MyClass {
public function getNamespace()
{
return get_class($this);
}
}
现在我们在同级目录下引用这个类:
<?php
function __autoload($name)
{
$file = realpath(__DIR__).'/'.$name.'.php';
if(file_exists($file))
{
require_once($file);
if(class_exists($name,false))
{
return true;
}
return false;
}
return false;
}
$obj = new MyClass();
echo $obj->getNamespace(); //输出 MyClass;
当你引用不存在的类时,__autoload就会被调用,并且你的类名会被作为参数传送过去(当你同时使用命名空间,包含命名空间部分会一起作为参数传送)。
下面我们把命名空间和自动加载类合并使用:
命名空间,PHP从5.3开始支持命名空间(namespace),这个在C语言里非常常见,刚开始我不理解,直到我完整接触一个项目,需要加载在不同目录下Class时,恍然大悟。OK,下面讲一讲自己的理解吧。
用不同的命名空间区别相同的函数名,同时命名空间也可以作为引入类文件的路径。
(在WEB项目中,当在client输入一个url时,一般会有一个入口文件,在这个文件里解析你要调用的controller,以及传递的参数,然后动态的加载相应类文件,这其实就牵扯到一点路由规则,当然还有另外一种,在项目初始化之初就把所有类文件全部加载在内存中,这样,每次请求响应都不用再加载,常驻内存后响应要快点。)
<?php
#首先建立一个MyClass目录,在该目录下新MyClass.php文件,代码如下。
namespace MyClass;
class MyClass {
public function getNamespace()
{
return get_class($this);
}
}
<?php
#在跟MyClass目录同级的目录下,新建一个文件,内容如下
function __autoload($name)
{
$class_path = str_replace('\\',DIRECTORY_SEPARATOR,$name); //把表示命名空间的分割符号,转换成表示目录结构的斜线
$file = realpath(__DIR__).'/'.$class_path.'.php';
if(file_exists($file))
{
require_once($file); //引入文件
if(class_exists($name,false)) //带有命名空间的类名
{
return true;
}
return false;
}
return false;
}
echo MyClass\MyClass::getNamespace(); //输出: MyClass\MyClass;
这样非常容易避免了,当一个项目非常大时,不停的require文件。
让我们再思考,有没有更工程化的解决方案?当然有,我们可以建立自己的 Autoload 类,在这个类里面自动加载不同的类(可以包含命名空间),需要引用其他类的文件,只需要显示写入这个 Autoload 类即可。
我们知道PHP可以实现自动加载,避免了繁重的体力活,代码更规范,整洁。那如果我们把这个自动加载再升华一下,变成自动加载类,每次只需要引入这个类,那么其他类就自动加载了,已经开源,仓库地址在这里。同时如果加载后可以使其常驻内存,那么效率又高了。。。
关于自动的基础部分可以参考这里
下面有几点需要注意
1.为了实现通用性,设置一个根目录,在大型项目中,目录结构比较复杂,每次加载都应该有一个起始开始的目录位置,这样才能向后兼容。当然也可以设置多个这种目录,放到数组里。
2.带有命名空间的类,需要把转义符转换成表示目录结构的斜线,加载的类是带有命名空间的,因为,该类是属于该命名空间下。
好了,下面贴上代码:
<?php
if(!defined('ROOTDIR'))
{
define('ROOTDIR',realpath(__DIR__.'/../')); //定义更目录
}
class Autoloader {
public static function myAutoload( $name )
{
$class_path = str_replace('\\',DIRECTORY_SEPARATOR,$name);
$file = ROOTDIR.'/'.$class_path.'.php';
if( file_exists( $file ) )
{
require_once( $file );
if( class_exists($name, false) )
{
return true;
}
}
return false;
}
}
spl_autoload_register('Autoloader::myAutoload');
自动加载的使用必须显示加载,它是加载其他类的加载器,我们已经重载了PHP的自动加载没有机制可以加载这个Autoloader,所以要 require_once('Autoloader.php');
很简单吧,其实这个可以做的更强大。
比如,我把这个做成一个中间件,命名为Bootstrap,这个可以用于不同项目,而这些项目可以位于不同或者同一个WEB目录下,这时需要一个静态的属性表示所有可能的应用目录或者不同WEB目录
public static $rootPath = array();
在加载时,就需要一个循环遍历每一个可能的目录,找到即加载。
同时,我们可以设置一个 setRootPath() 方法,用于设置不同项目的目录。
public function setRootPath( $path )
{
//do something
return $this;
}
这样就能实现链式操作的添加,是不是很神奇。。。
第二部分:spl_autoload_register类自动加载函数
接下来让我们要在含有命名空间的情况下去实现自动加载。这里我们使用 spl_autoload_register() 函数来实现,这需要你的 PHP 版本号大于 5.12。
spl_autoload_register 函数的功能就是把传入的函数(其实就是一系列的autoload函数)注册到 SPL __autoload 函数队列中,并移除系统默认的 __autoload() 函数。
一旦调用 spl_autoload_register() 函数,当调用未定义类时,系统就会按顺序调用注册到 spl_autoload_register() 函数的所有函数,而不是自动调用 __autoload() 函数。
现在,我们来创建一个 Linux 类,它使用 os 作为它的命名空间(建议文件名与类名保持一致):
namespace os; // 命名空间
class Linux // 类名
{
function __construct()
{
echo '<h1>' . __CLASS__ . '</h1>';
}
}
接着,在同一个目录下新建一个 PHP 文件,使用 spl_autoload_register 以函数回调的方式实现自动加载:
spl_autoload_register(function ($class) { // class = os\Linux
/* 限定类名路径映射 */
$class_map = array(
// 限定类名 => 文件路径
'os\\Linux' => './Linux.php',
);
/* 根据类名确定文件名 */
$file = $class_map[$class];
/* 引入相关文件 */
if (file_exists($file)) {
include $file;
}
});
new \os\Linux();
这里我们使用了一个数组去保存类名与文件路径的关系,这样当类名传入时,自动加载器就知道该引入哪个文件去加载这个类了。
但是一旦文件多起来的话,映射数组会变得很长,这样的话维护起来会相当麻烦。如果命名能遵守统一的约定,就可以让自动加载器自动解析判断类文件所在的路径。
第三部分:既然有__autoload函数来实现了类的自动加载,那么为什么要有spl_autoload_register的存在??
如果在一个系统的实现中,如果需要使用很多其它的类库,这些类库可能是由不同的开发人员编写的,其类名与实际的磁盘文件的映射规则不尽相同。这时如果要实现类库文件的自动加载,就必须在__autoload()函数中将所有的映射规则全部实现,这样的话__autoload()函数有可能 会非常复杂,甚至无法实现。最后可能会导致__autoload()函数十分臃肿,这时即便能够实现,也会给将来的维护和系统效率带来很大的负面影响。在这种情况下,难道就没有更简单清晰的解决办法了吧?答案当然是:NO!
spl_autoload_register() 满足了此类需求。 它实际上创建了 autoload 函数的队列,按定义时的顺序逐个执行。相比之下, __autoload() 只可以定义一次。
bool spl_autoload_register ([ callable $autoload_function [, bool $throw = true [, bool $prepend = false ]]] )
我们继续改写上面那个例子:
<?php
function loader($class){
$file = $class . '.php';
if (is_file($file)) {
require_once($file);
}
}
spl_autoload_register('loader');
$a = new A();
或者直接使用匿名函数:
<?php
spl_autoload_register(function($file){
$file = $class . '.php';
if (is_file($file)) {
require_once($file);
}
});
$a = new A();
这样子也是可以正常运行的,这时候php在寻找类的时候就没有调用__autoload而是调用我们自己定义的函数loader了。同样的道理,下面这种写法也是可以的:
<?php
class Loader {
public static function loadClass($class){
$file = $class . '.php';
if (is_file($file)) {
require_once($file);
}
}
}
spl_autoload_register(array('Loader', 'loadClass'));
//spl_autoload_register(array(__CLASS__, 'loadClass'));
//spl_autoload_register(array($this, 'loadClass'));
$a = new A();
第四部分:yii的类自动加载机制的实现
Yii的类自动加载,依赖于PHP的 spl_autoload_register() , 注册一个自己的自动加载函数(autoloader),并插入到自动加载函数栈的最前面,确保Yii的autoloader会被最先调用。
类自动加载的这个机制的引入要从入口文件 index.php 开始说起:
<?php
defined('YII_DEBUG') or define('YII_DEBUG', false);
defined('YII_ENV') or define('YII_ENV', 'prod');
// 这个是第三方的autoloader
require(__DIR__ . '/../../vendor/autoload.php');
// 这个是Yii的Autoloader,放在最后面,确保其插入的autoloader会放在最前面
require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');
// 后面不应再有autoloader了
require(__DIR__ . '/../../common/config/aliases.php');
$config = yii\helpers\ArrayHelper::merge(
require(__DIR__ . '/../../common/config/main.php'),
require(__DIR__ . '/../../common/config/main-local.php'),
require(__DIR__ . '/../config/main.php'),
require(__DIR__ . '/../config/main-local.php')
);
$application = new yii\web\Application($config);
$application->run();
这个文件主要看点在于第三方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
spl_autoload_register(['Yii', 'autoload'], true, true);
// 下面的语句读取了一个映射表
Yii::$classMap = include(__DIR__ . '/classes.php');
Yii::$container = new yii\di\Container;
这段代码,调用了 spl_autoload_register(['Yii', 'autoload', true, true]) ,将 Yii::autoload() 作为autoloader插入到栈的最前面了。并将 classes.php 读取到 Yii::$classMap 中,保存了一个映射表。
在上面的代码中,Yii类是里面没有任何代码,并未对 BaseYii::autoload() 进行重载,所以,这个 spl_autoload_register() 实际上将 BaseYii::autoload() 注册为autoloader。如果,你要实现自己的autoloader,可以在 Yii 类的代码中,对 autoload() 进行重载。
在调用 spl_autoload_register() 进行autoloader注册之后,Yii将 calsses.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?");
}
}
从这段代码来看Yii类自动加载机制的运作原理:
• 检查 $classMap[$className] 看看是否在映射表中已经有拟加载类的位置信息;
• 如果有,再看看这个位置信息是不是一个路径别名,即是不是以 @ 打头, 是的话,将路径别名解析成实际路径。 如果映射表中的位置信息并非一个路径别名,那么将这个路径作为类文件的所在位置。 类文件的完整路径保存在 $classFile ;
• 如果 $classMap[$className] 没有该类的信息, 那么,看看这个类名中是否含有 \ , 如果没有,说明这是一个不符合规范要求的类名,autoloader直接返回。 PHP会尝试使用其他已经注册的autoloader进行加载。 如果有 \ ,认为这个类名符合规范,将其转换成路径形式。 即所有的 \ 用 /替换,并加上 .php 的后缀。
• 将替换后的类名,加上 @ 前缀,作为一个路径别名,进行解析。 从别名的解析过程我们知道,如果根别名不存在,将会抛出异常。 所以,类的命名,必须以有效的根别名打头:
// 有效的类名,因为@yii是一个已经预定义好的别名
use yii\base\Application;
// 无效的类名,因为没有 @foo 或 @foo/bar 的根别名,要提前定义好
use foo\bar\SomeClass;
• 使用PHP的 include() 将类文件加载进来,实现类的加载。
从其运作原理看,最快找到类的方式是使用映射表。 其次,Yii中所有的类名,除了符合规范外,还需要提前注册有效的根别名。
运用自动加载机制
在入口脚本中,除了Yii自己的autoloader,还有一个第三方的autoloader:
require(__DIR__ . '/../../vendor/autoload.php');
这个其实是Composer提供的autoloader。Yii使用Composer来作为包依赖管理器,因此,建议保留Composer的autoloader,尽管Yii的autoloader也能自动加载使用Composer安装的第三方库、扩展等,而且更为高效。但考虑到毕竟是人家安装的,人家还有一套自己专门的规则,从维护性、兼容性、扩展性来考虑,建议保留Composer的autoloader。
如果还有其他的autoloader,一定要在Yii的autoloader注册之前完成注册,以保证Yii的autoloader总是最先被调用。
如果你有自己的autoloader,也可以不安装Yii的autoloaer,只是这样未必能有Yii的高效,且还需要遵循一套类似的类命名和加载的规则。就个人的经验而言,Yii的autoloader完全够用,没必要自己重复造轮子。
至于Composer如何自动加载类文件,这里就不过多的占用篇幅了。可以看看 Composer的文档 。
refer
PHP自动加载类__autoload()浅谈
PHP 实现自动加载器(Autoloader)
PHP 命名空间与自动加载机制介绍
php自动加载方式集合