php的类自动加载

作者: 舒小贱 | 来源:发表于2017-12-06 11:27 被阅读61次

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

    相关文章

      网友评论

        本文标题:php的类自动加载

        本文链接:https://www.haomeiwen.com/subject/mnykixtx.html