php的类自动加载

本文分为两部分:第一部分讲__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自动加载方式集合

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容