php自动加载机制从0到优化

4 篇文章 0 订阅
2 篇文章 0 订阅

前言

本文是《自制php框架》之自动加载篇,笔者参照tp5框架的自动加载相关源码,写了几个p1~p4四个demo(放在我的github了),基本体现了从0到成型框架的自动加载的编写过程。文章篇幅很长,如果你属于以下情况,建议看下:

  • 用过php框架,但不懂为何:只要use app\model\User(没有include或require)就能直接用User类。

  • 理解php是通过spl_autoload_register实现自动加载的,但心血来潮看了一下某个框架的源码,不理解大型一点的框架的自动机制代码为何能写那么长,是为了解决什么问题。

  • 想理解composer的自动加载如何实现。

正文

背景知识

  • 非自动加载的使用类的流程是先引入类所在文件,然后访问该类。
  • php实现自动加载的核心是:当访问在其他文件定义的类时,会调用我们指定的处理函数,在该函数内引入类所在文件。
  • 这个自动加载处理函数是通过spl_autoload_register注册(即指定)的,可以注册多个
    • 为什么需要注册多个?因为使用第三方包时,一般需要注册该包的自动加载处理函数,实现加载该包的类。
    • 有多个就涉及顺序了,存储自动加载函数的数据结构是队列,即先注册的,优先使用,找不到再调后面的处理函数。当然,spl_autoload_register(callback, $prepend),设置第二个参数为true,即可排到队首。
    • 最初的自动加载处理方式是:直接在__autoload(){}写处理代码,但明显不能应对上面说的有多个自动加载处理函数的情况。
  • 命名空间:
    • 为什么要有命名空间?能区分同名的类
    • 与自动加载的关系?自动加载处理函数里就是根据命名空间找到类所在文件的路径的。
  • 最终期望实现的效果是:在A文件,想访问定义在B文件的类,只需
  1. 在B文件声明命名空间
  2. 在A文件use,然后访问。

P1:自动加载简单尝试

根据上面的背景知识,最直观能想到的,就是P1这种实现。也是网上大多数博文都有写到的。

先看看整体目录结果,后面的p2~p4目录结构相同。

p1
├── app		## 应用目录,开发者主要在这层写
│   ├── controller
│   │   └── User.php
│   └── model
│       └── User.php
├── fool	## 框架类库目录
│   └── Loader.php	## 自动加载类
├── index.php	## 入口文件
└── vendor	## 第三方包目录
    └── pack1
        └── A.php	## 第三个包pack1下的A文件,里面有A类
  1. 首先在入口文件/index.php定义根目录路径和调用自动加载类

    <?php
    define("DS", DIRECTORY_SEPARATOR);	 // linux下是/,windows是/或\
    define("EXT", '.php');
    define("ROOT_PATH", __DIR__ . DS);		// 项目根目录
    
    // 注册自动加载机制
    require ROOT_PATH . 'fool/Loader.php';
    \fool\Loader::register();
    
  2. 然后在/fool/Loader.php写具体的加载处理:

    1. 注意一点:autoload($class)的参数$class是自动传入的,其值就是想访问但找不到的类的完整类名(即包含命名空间部分的)。打印一下就知道了~
    2. 核心是findFile(),命名空间格式是namespace app\controller;,linux下文件目录路径是app/contoller,要找到文件,将传入命名空间的\转成DS,再拼上.php,就是了。
    <?php
        
    namespace fool;
    
    class Loader
    {
        /*  
         * 注册自动加载处理函数
         * @return void
         * */
        public static function register() 
        {   
            spl_autoload_register("fool\\Loader::autoload", true, true);
        }   
    
        /*  
         * 自动加载处理函数
         * @param  string  $class 类名
         * @return bool
         * */
        public static function autoload($class) 
        {   
            if ($file = self::findFile($class)) {
                return require $file;
            }   
    
            return false;
        }   
        
        /*  
         * 查找文件
         * @param  string $class 类名
         * @return bool|string    
         * */
        private static function findFile($class) 
        {   
            return ROOT_PATH . strtr($class, '\\', '/') . EXT;
        }   
    }
    
  3. 测试:

    1. /app/controller/User.php声明User类,当然是要声明命名空间的。然后通过use方式,访问定义在app/model/User.phpUser类。
    <?php
    
    namespace app\controller;
    
    use app\model\User as UserModel;
    
    class User 
    {
        public function index() 
        {   
            echo "this is controller User function index <br />\n";
            $model = new UserModel();
            $model->getList();
        }   
    }
    
    1. /app/model/User.php
    <?php
    
    namespace app\model;
    
    class User 
    {
        public function getList() 
        {   
            echo "this is model User function getList <br /> \n";
        }   
    }
    
    1. 在入口文件/index.php访问
    // new与目录对应的命名空间,成功
    use app\controller\User;
    
    $user = new User();
    $user->index();
    
    1. 运行php index.php,或通过web方式访问index.php。看到打印如下:表明访问成功
    this is controller User function index 
    this is model User function getList 
    
  4. 似乎,自动加载就这么简单的实现了,上面写的最终实现效果也实现了。但,你也许有2个疑惑:

    1. 写了一堆代码,与最原始方式:用常量定义根目录,访问写在其它文件的类前require ROOT_PATH . 文件路径,再访问。似乎区别不大啊,并没简化多少工作量,访问前还是要use,甚至多出一步,在类头声明命名空间,这样做的意义在哪?

      在类头声明命名空间是为了避免类重名,即使通过原始方式,也应该要在类头声明命名空间,访问前也要use。

      即原始方式实际也是:require -> use -> 访问

      而自动加载是:use -> 访问

      use 是必需的,否则,程序怎么可能知道你要访问的是哪个类。

    2. 嗯,自动加载的好处我体会到了(省去了require步骤),那么,为什么那些框架的自动加载处理代码有那么长呢,为了解决什么问题?tp5.0自动加载源码

  5. 其中一个要解决的问题是,也就是P1方式的致命缺陷,如果文件路径与声明的命名空间不对应。当使用第三方包时~看例子吧。

    把第三方包放在/vendor目录下,/vendor/pack1为一个第三方包,在/vendor/pack1/A.php写,第三方包的命名空间不可能是vendow/pack1的,别人用了存放第三方包的目录名未必是vendor,因此第三方包基本都是这种格式。

    <?php
    
    namespace pack1;
    
    class A
    {
        public function work()
        {   
            echo "this is a extra package pack1, class A function work was called <br /> \n";
        }   
    }
    
  6. 访问试试,在/index.php

    // new目录与命名空间不对应的 (项目目录/vendor/pack1/A.php, 命名空间pack1\A) 失败
    use pack1\A;
    
    $p1 = new A();
    $p1->work();   
    

    报错找不到

    PHP Warning:  require(/data/autoload/p1/pack1/A.php): failed to open stream: No such file or directory in /data/autoload/p1/fool/Loader.php on line 24
    

    如何解决命名空间与所在文件路径不对应情况?

P2:添加注册命名空间机制

use pack1\A要引入/vendor/pack1/A.php,很容易想到,只要在自动加载处理函数函数里,将pack1 替换成/vendor/pack1即可,即需要加多两个步骤:

  1. 注册命名空间:即将pack1/vendor/pack1的映射关系存到变量里。
  2. 找文件时:传入命名空间 + 映射关系 -> 文件路径

上面的代码不变,改的只有/fool/Loader.php

  1. 注册命名空间

    1. 添加私有的$prefixDirsPsr4变量,存命名空间与目录的映射关系。
    2. 添加私有的addPsr4()方法,将映射关系存储到$prefixDirsPsr4变量。
    3. 添加public的addNamespace()方法, 目的有
      1. 提供对外注册命名空间接口
      2. 使注册命名空间传参方便些。
       private static $prefixDirsPsr4 = [];
       
       public static function addNamespace($namespace, $path = '')
        {
            if (is_array($namespace)) {
                foreach ($namespace as $prefix => $paths) {
                    self::addPsr4($prefix, $paths, true);
                }
            } else {
                self::addPsr4($namespace, $path, true);
            }
        }    
    
       private static function addPsr4($prefix, $paths, $prepend = false)
        {
            if (!isset(self::$prefixDirsPsr4[$prefix])) {
                // 注册新的命名空间
                self::$prefixDirsPsr4[$prefix] = (array) $paths;
            } else {
                // 为已有命名空间添加对应目录
                self::$prefixDirsPsr4[$prefix] = $prepend ?
                    array_merge((array) $paths, self::$prefixDirsPsr4[$prefix]) :
                    array_merge(self::$prefixDirsPsr4[$prefix], (array) $paths);
            }
        }
    
  2. 注册框架必须的命名空间:

        public static function register()
        {
            spl_autoload_register("fool\\Loader::autoload", true, true);
        
            // 添加命名空间 对应目录
            self::addNamespace([
                'app' => APP_PATH,
                'fool' => FOOL_PATH,
            ]);
        }   
    
  3. 修改找文件方式:思路是识别出传入命名空间里首次出现的/的索引,将前面替换成对应文件路径,再拼接后面。

        private static function findFile($class) 
        {   
            // 先直接 命名空间 转成 路径
            $logicalPathPsr4 = strtr($class, '\\', DS) . EXT;
    
            // 根据(命名空间 与 目录)映射 替换前缀
            $len = strpos($logicalPathPsr4, '/');
            $cPrefix = substr($logicalPathPsr4, 0, $len);
            $follow = substr($logicalPathPsr4, $len+1);
        
            foreach (self::$prefixDirsPsr4 as $prefix => $dirs) {
                if ($prefix == $cPrefix) {
                    foreach ($dirs as $dir) {
                        if (is_file($file = $dir . $follow)) {
                            return $file;
                        }   
                    }   
                }   
            }   
    
            return false;
        }   
    
  4. 与P1同样调用的代码,在/index.php

    // new与目录对应的命名空间,成功
    use app\controller\User;
    
    $user = new User();
    $user->index();
    

    运行,成功

    this is controller User function index 
    this is model User function getList 
    
  5. 注册额外的命名空间,在/index.php。这就是为什么addNamespace()方法了要设为public。

    // 设置命名空间pack1 对应 目录 vendow/pack1
    \fool\Loader::addNamespace('pack1', ROOT_PATH . 'vendor/pack1'. DS);
    
  6. new命名空间与文件所在路径不对应的类,在/index.php

    // new目录与命名空间不对应的 (项目目录/vendor/pack1/A.php, 命名空间pack1\A) 也成功
    use pack1\A;
    
    $p1 = new A();
    $p1->work();
    

    运行,成功

    this is controller User function index 
    this is model User function getList 
    this is a extra package pack1, class A function work was called 
    

P3:完善注册命名空间机制

p3实现的效果与p2基本相同,p3基本就是tp5那套,而tp5那套很像composer那套。

  1. 用了官方的规范:对于映射关系中命名空间结尾是否要加\,目录结尾是否要加/,想必很乱,按tp5的来,这里统一:

    1. 命名空间结尾加\:如fool\app\
    2. 目录结尾不加/:如/data/autoload/p3/fool
  2. 其它代码不变,修改/fool/Loader.php

    1. 添加$prefixLengthsPsr4:变量的结构是,按注册的命名空间的首字母划分,存了其长度

      Array
      (
          [a] => Array
              (
                  [app\] => 4
                  [aaaaa\] => 6
              )
          [f] => Array
              (
                  [fool\] => 5
              )
      )
      
    2. addPsr4()findFile()方法也改成对应那套

          private static $prefixLengthsPsr4 = []; 
      
          private static function addPsr4($prefix, $paths, $prepend = false)
          {
              if (!isset(self::$prefixDirsPsr4[$prefix])) {
                  // 注册新的命名空间
                  self::$prefixDirsPsr4[$prefix] = (array) $paths;
                  // 记录前缀长度
                  $length = strlen($prefix);
                  if ('\\' !== $prefix[$length - 1]) {
                      // PSR-4规范,非类的命名空间应该以\结尾
                      echo 'A non-empty PSR-4 prefix must end with a namespace separator.';
                  }
                  self::$prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
              } else {
                  // 为已有命名空间添加对应目录
                  self::$prefixDirsPsr4[$prefix] = $prepend ?
                      array_merge((array) $paths, self::$prefixDirsPsr4[$prefix]) :
                      array_merge(self::$prefixDirsPsr4[$prefix], (array) $paths);
              }
          }
      
          private static function findFile($class)
          {
              // 先直接 命名空间 转成 路径
              $logicalPathPsr4 = strtr($class, '\\', DS) . EXT;
      
              // 根据(命名空间 与 目录)映射 替换前缀
              $first = $class[0];
              if (isset(self::$prefixLengthsPsr4[$first])) {
                  foreach (self::$prefixLengthsPsr4[$first] as $prefix => $length) {
                      if (0 === strpos($class, $prefix)) {
                          foreach (self::$prefixDirsPsr4[$prefix] as $dir) {
                              if (is_file($file = $dir . DS . substr($logicalPathPsr4, $length))) {
                                  return $file;
                              }
                          }
                      }
                  }
              }
      
              return false;
          }
      

笔者也还没能理解这样写好处在哪,猜想是注册时,即新增数据时,添加索引,查找时,根据索引找效率会更高,(自动加载确实应该考虑效率)。但findFile时始终要遍历去找,与P2相比,貌似并不能减少循环次数。

P4:完善自动加载机制

上面P2,P3基本没问题了,只要给类定义好命名空间,不管类所在文件放在哪,只要将命名空间注册,用前use下,就能用了。

但还能优化,从效率方面考虑。

如,当框架成型,每次请求运行,都必须要找自定义异常处理类,日志类等,框架目录结果基本是固定的,但现在的方式对这种命名空间与目录确定的关系,还是要遍历去找, 是否觉得这部分遍历是多余的?同理,当项目开发完,大部分文件只需小改,文件路径不会大改,是否可以省去遍历查找。

答案是肯定的:只需记录完整命名空间 => 文件路径。下面第一点就是相应处理。下面的第二点是可简单理解为设置默认目录,如果都找不到,就去默认目录里找。第三点:我目前还没用过,看tp5有就也加上去了。

下面是P4的所有优化:

1. 添加类名映射
  1. /fool/Loader.php

    1. 添加$classMap变量:存储完整命名空间 与 文件路径 的映射关系,结构是:

      Array
      (
          [TestClassMap] => /data/autoload/p4/classMap/TestClassMap.php
      )
      
    2. 添加public的注册类名映射方法addClassMap()

    3. findFile()时,优先通过$classMap判断。

        private static $classMap = []; 
    
        public static function addClassMap($class, $map = '')
        {
            if (is_array($class)) {
                self::$classMap = array_merge(self::$classMap, $class);
            } else {
                self::$classMap[$class] = $map;
            }
        }
    
        private static function findFile($class)
        {
            // 类库映射 具体指定的,优先级最高
            if (isset(self::$classMap[$class])) {
                return self::$classMap[$class];
            }
            
            
            // 遍历prefixDirsPsr4找...
            
            // 找不到记录一下映射为false, 并返回false
            return self::$classMap[$class] = false;
        }
    
  2. /classMap/TestClassMap.php,加

    <?php
    
    class TestClassMap
    {
        public function work()
        {   
            echo "this is classMap class A function work <br /> \n";
        }   
    }
    
  3. index.php注册类名映射,并运行之

    // 注册类库映射
    Loader::addClassMap('TestClassMap', ROOT_PATH . 'classMap/TestClassMap.php');
    
    $tcp = new TestClassMap();
    $tcp->work();
    
    // 输出
    this is classMap class A function work 
    
2.添加回退目录:即默认目录
  1. /fool/Loader.php添加,$fallbackDirsPsr4结构:

    Array
    (
        [0] => /data/autoload/p4/extend
    )
    
        private static $fallbackDirsPsr4 = []; 
    
        public static function register()
        {
            // ...
            
            // 自动加载extend目录
            self::$fallbackDirsPsr4[] = rtrim(EXTEND, DS);   
        }   
    
        private static function findFile($class)
        {
            // 类库映射 查找,优先级最高
    		// ...
            
            // 根据prefixDirsPsr4找
            // ...
    
            // 指定目录找不到 从PSR-4回退目录(也可理解为默认目录)找
            foreach (self::$fallbackDirsPsr4 as $dir) {
                if (is_file($file = $dir . DS . $logicalPathPsr4)) {
                    return $file;
                }
            }
    
            // 找不到记录一下映射为false, 并返回false
            // ...
        }
    
  2. 添加/extend/e1/A.php,写上

    <?php
    
    namespace e1; 
    
    class A 
    {
        public function work() 
        {   
            echo "this is extend e1 class A function work <br /> \n";
        }   
    }
    
  3. /index.php

    // new扩展目录的类
    use e1\A as eA; 
    $e = new eA();
    $e->work();
    

    运行,输出

    this is extend e1 class A function work
    
3. 添加类别名
  1. /fool/Loader.php,添加$namespaceAlias,结构:

    Array
    (
        [model] => app\model
    )
    
        private static $namespaceAlias = []; 
    
        public static function addNamespaceAlias($namespace, $original = '')
        {
            if (is_array($namespace)) {
                self::$namespaceAlias = array_merge(self::$namespaceAlias, $namespace);
            } else {
                self::$namespaceAlias[$namespace] = $original;
            }
        }
    
        public static function autoload($class) 
        {   
            // 检测命名空间别名,若匹配,则返回(并不加载)后面再进来findFile后加载
            if (!empty(self::$namespaceAlias)) {
                $length = strpos($class, '\\');
                $prefix = substr($class, 0, $length);
                if (isset(self::$namespaceAlias[$prefix])) {
                    $original = self::$namespaceAlias[$prefix] . substr($class, $length);
                    if (class_exists($original)) {
                        return class_alias($original, $class, false);
                    }
                }
            }
    
            if ($file = self::findFile($class)) {
                return require $file;
            }
    
            return false;
        }
    
  2. /app/model/Goods.php

    <?php
    
    namespace app\model;
    
    class Goods
    {
        public function getList() 
        {   
            echo "this is model Goods function getList <br /> \n";
        }   
    }
    
  3. /index.php,加

    // 添加别名
    Loader::addNamespaceAlias('model', 'app\model');
    use model\Goods;    // 使用了别名找类
    $goods = new Goods();
    $goods->getList();
    

    运行之,输出

    this is model Goods function getList 
    

P5

添加composer的自动加载处理

P6

添加PSR0规范的处理

总结

P4基本就能用了,添加composer处理和PSR-0处理以后有时间再写了。简单看了composer的Loader.php,大概也是这回事。而PSR-0,感觉写了相关处理也用不上。

额外写一句:php的自动加载花了2天多来理解,写demo并总结,以后看其它语言的自动加载,理解应该不难了。

通过Loader类的属性来总结下吧,实在无力写了。

重要的:

  1. prefixDirsPsr4
  2. classMap

次要的:

  1. prefixLengthsPsr4
  2. fallbackDirsPsr4
  3. namespaceAlias

github源码~

相关链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值