composer的源码分析

composer的源码分析

   在上一篇文章如何用composer来构建自己的MVC项目中,记录了如何使用composer来构建项目。那么它的实现原理是什么呢?我们通过源码来进行分析。

   当前的目录结构如下:

   

   composer.json内容如下:

 1 {
 2     "name": "hxq/composer-analysis",
 3     "authors": [
 4         {
 5             "name": "xxx",
 6             "email": "10033333@qq.com"
 7         }
 8     ],
 9     "require": {},
10     "autoload": {
11         "classmap": [
12             "app/Library/Crypt",
13             "app/Library/Phonecrypt",
14             "app/Library/Highlight"
15         ],
16         "psr-4": {
17             "App\\": "app/"
18         },
19         "files": [
20             "app/Helpers/functions.php"
21         ]
22     }
23 }

  下面就是整个分析的过程啦!  

 1.  启动

  入口文件index.php

1 <?php
2 define('APP_DEBUG', true);  //开启调试模式
3 date_default_timezone_set("Asia/Shanghai");
4 // 入口文件中利用composer来实现自动加载功能
5 require __DIR__ . '/vendor/autoload.php';//自动加载

   2.  autoload.php

  具体的分析在代码注释里面:

1 <?php
2 
3 // autoload.php @generated by Composer
4 // autoload.php不负责具体功能逻辑,只做两件事:自动加载类的初始化和注册
5 // composer真正开始的地方
6 
7 require_once __DIR__ . '/composer/autoload_real.php';
8 
9 return ComposerAutoloaderInit03d4bf55ac53dfdc34a9b856a0bd37a2::getLoader();

  3.  autoload_real.php  自动加载功能的引导类

  我们先看下这个类的结构,如图:

   程序主要调用了这个类的getLoader方法,因此这个方法是我们分析的重点。

   具体的分析在代码注释里面:

 1 <?php
 2 
 3 // autoload_real.php @generated by Composer
 4 // 自动加载功能的引导类:composer加载类的初始化(顶级命名空间与文件路径映射初始化)和注册
 5 
 6 class ComposerAutoloaderInit03d4bf55ac53dfdc34a9b856a0bd37a2
 7 {
 8     private static $loader;
 9 
10     public static function loadClassLoader($class)
11     {
12         if ('Composer\Autoload\ClassLoader' === $class) {
13             require __DIR__ . '/ClassLoader.php';
14         }
15     }
16 
17     /**
18      * @return \Composer\Autoload\ClassLoader
19      */
20     public static function getLoader()
21     {
22         // 1.单例模式:自动加载类只能有一个
23         if (null !== self::$loader) {
24             return self::$loader;
25         }
26 
27         // 2.实例化一个自动加载的核心类对象
28         spl_autoload_register(array('ComposerAutoloaderInit03d4bf55ac53dfdc34a9b856a0bd37a2', 'loadClassLoader'), true, true);
29         self::$loader = $loader = new \Composer\Autoload\ClassLoader();
30         spl_autoload_unregister(array('ComposerAutoloaderInit03d4bf55ac53dfdc34a9b856a0bd37a2', 'loadClassLoader'));
31 
32         // 3.自动加载类的初始化
33         // 静态初始化只支持 PHP5.6 以上版本并且不支持 HHVM 虚拟机
34         $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
35         if ($useStaticLoader) {
36             // 方式一:静态初始化
37             require_once __DIR__ . '/autoload_static.php';
38 
39             call_user_func(\Composer\Autoload\ComposerStaticInit03d4bf55ac53dfdc34a9b856a0bd37a2::getInitializer($loader));
40         } else {
41             
42             // 方式二:调用核心类接口初始化
43 
44             // PSR0标准
45             $map = require __DIR__ . '/autoload_namespaces.php';
46             foreach ($map as $namespace => $path) {
47                 $loader->set($namespace, $path);
48             }
49 
50             // PSR4标准
51             $map = require __DIR__ . '/autoload_psr4.php';
52             foreach ($map as $namespace => $path) {
53                 $loader->setPsr4($namespace, $path);
54             }
55 
56             $classMap = require __DIR__ . '/autoload_classmap.php';
57             if ($classMap) {
58                 $loader->addClassMap($classMap);
59             }
60         }
61 
62         // 4.注册(负责顶层以下的命名空间的映射规则)
63         $loader->register(true);
64 
65         // 5.自动加载全局函数:分为两种方式
66         if ($useStaticLoader) {
67             // 方式一:静态初始化
68             $includeFiles = Composer\Autoload\ComposerStaticInit03d4bf55ac53dfdc34a9b856a0bd37a2::$files;
69         } else {
70             // 方式二:普通初始化
71             $includeFiles = require __DIR__ . '/autoload_files.php';
72         }
73         foreach ($includeFiles as $fileIdentifier => $file) {
74             composerRequire03d4bf55ac53dfdc34a9b856a0bd37a2($fileIdentifier, $file);
75         }
76 
77         return $loader;
78     }
79 }
80 
81 function composerRequire03d4bf55ac53dfdc34a9b856a0bd37a2($fileIdentifier, $file)
82 {
83     if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
84         require $file;
85 
86         $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
87     }
88 }

   由上面的代码注释分析,我们可以看到,自动加载引导类分成5个部分

       1)单例模式

       2)实例化一个自动加载的核心类ClassLoader对象

       3)自动加载核心类的初始化

       4)注册(负责顶层以下的命名空间的映射规则)

       5)自动加载全局函数

      其实这里面最重要的就是自动加载核心类对象的初始化以及注册。

      除了以上之外,这里还有几个需要关注的地方:

      关注点1: autoload_real.php 中的类名为 ComposerAutoloaderInit... 此处为了防止用户自定义类名跟这个类重复冲突,为什么采用的是加上哈希值而不是定义命名空间呢?

      分析:之所以这里没有定义命名空间,是因为命名空间一般都是为了复用,而这个类只需要运行一次即可,以后也不会用得到,用哈希值更加合适

      关注点2:代码28-30行中,实例化自动加载核心类对象时,为什么不直接require核心类ClassLoader?

      分析:防止用户也定义了 \Composer\Autoload\ClassLoader 命名空间,导致自动加载错误文件。这个地方利用spl_autoload_register,最后一个参数是true,作用是将自定义的loadClassLoader()添加到

__autoload函数堆栈的队列之首。确保能够正确地加载到ClassLoader类。

      关注点3:为什么ClassLoader类是用命名空间来定义的,而不是像autoload_real或者autoload_static类一样加个哈希值呢?

      分析:ClassLoader这个类是可以复用,框架允许用户使用这个类。

      关注点4:73-75行,为什么不直接 require $includeFiles 里面的每个文件名,而要用类外面的函数 composerRequire... ?

      分析:原因有两个

      1) 是为了避免和用户定义函数冲突 

      2) 防止用户在全局函数所在的文件写 $this 或者 self。

            假如 $includeFiles 有个 app/helper.php 文件,这个 helper.php 文件的函数外有一行代码: $this->foo()。如果引导类在 getLoader() 函数直接 require($file),那么引导类就会运行这句代码,调用自己的 foo() 函数,这显然是错的。

     关注点5:81-88行,自动加载全局函数的实现中,为什么要用 hash 作为 $fileIdentifier?

     分析:这个变量是用来控制全局函数只被 require 一次的,那为什么不用 require_once 呢?事实上 require_once 比 require 效率低很多(具体原因参考文章再一次, 不要使用(include/require)_once),使用全局变量 $GLOBALS 这样控制加载会更快。

      4.  autoload_static.php 用于静态初始化的类

      注意:这里的类名与autload_real类相似,也是用ComposerStaticInit加上哈希值组成的。不同的地方是autoload_static还使用了命名空间的定义。

      使用静态初始化是有条件的:只支持 PHP 5.6 以上版本、不支持 HHVM 虚拟机、不存在 Zend-encoded file

      具体的分析在代码注释里面:

 1 <?php
 2 
 3 // autoload_static.php @generated by Composer
 4 // 用于静态初始化的类
 5 
 6 namespace Composer\Autoload;
 7 
 8 class ComposerStaticInit03d4bf55ac53dfdc34a9b856a0bd37a2
 9 {
10     public static $files = array (
11         'bbaaff3a6e68bfb5889450e10efe9617' => __DIR__ . '/../..' . '/app/Helpers/functions.php',
12     );
13 
14     // PSR4标准顶级命名空间映射数组:分为$prefixLengthsPsr4和$prefixDirsPsr4
15     // 用命名空间第一个字母作为前缀索引,对应的是一维数组,key为顶级命名空间,value为顶级命名空间的长度
16     // PSR4 标准是用顶级命名空间目录替换顶级命名空间,所以获得顶级命名空间的长度很重要
17     public static $prefixLengthsPsr4 = array (
18         'A' => 
19         array (
20             'App\\' => 4,
21         ),
22     );
23 
24     // 顶级命名空间的映射目录数组,注意:映射目录可能不止一条
25     public static $prefixDirsPsr4 = array (
26         'App\\' => 
27         array (
28             0 => __DIR__ . '/../..' . '/app',
29         ),
30     );
31 
32     // 命名空间映射:直接命名空间全名与目录的映射
33     public static $classMap = array (
34         'ErrorCode' => __DIR__ . '/../..' . '/app/Library/Crypt/errorCode.php',
35         'ErrorCode1' => __DIR__ . '/../..' . '/app/Library/Phonecrypt/errorCode1.php',
36         'Highlight\\Autoloader' => __DIR__ . '/../..' . '/app/Library/Highlight/Autoloader.php',
37         'Highlight\\Highlighter' => __DIR__ . '/../..' . '/app/Library/Highlight/Highlighter.php',
38         'Highlight\\JsonRef' => __DIR__ . '/../..' . '/app/Library/Highlight/JsonRef.php',
39         'Highlight\\Language' => __DIR__ . '/../..' . '/app/Library/Highlight/Language.php',
40         'PKCS7Encoder' => __DIR__ . '/../..' . '/app/Library/Crypt/pkcs7Encoder.php',
41         'Prpcrypt' => __DIR__ . '/../..' . '/app/Library/Crypt/pkcs7Encoder.php',
42         'SHA1' => __DIR__ . '/../..' . '/app/Library/Crypt/sha1.php',
43         'WXBizDataCrypt1' => __DIR__ . '/../..' . '/app/Library/Phonecrypt/wxBizDataCrypt1.php',
44         'WXBizMsgCrypt' => __DIR__ . '/../..' . '/app/Library/Crypt/wxBizMsgCrypt.php',
45         'XMLParse' => __DIR__ . '/../..' . '/app/Library/Crypt/xmlparse.php',
46     );
47 
48     /**
49      * 这个方法是静态初始化类的核心
50      * 将自己类中的顶级命名空间映射给了ClassLoader类
51      * @param ClassLoader $loader
52      * @return mixed
53      * 返回一个已经绑定$this对象和类作用域的闭包(说明:类作用域可以访问私有属性)
54      */
55     public static function getInitializer(ClassLoader $loader)
56     {
57         // 匿名函数的绑定功能,返回的是一个匿名函数(闭包)
58         return \Closure::bind(function () use ($loader) {
59             $loader->prefixLengthsPsr4 = ComposerStaticInit03d4bf55ac53dfdc34a9b856a0bd37a2::$prefixLengthsPsr4;
60             $loader->prefixDirsPsr4 = ComposerStaticInit03d4bf55ac53dfdc34a9b856a0bd37a2::$prefixDirsPsr4;
61             $loader->classMap = ComposerStaticInit03d4bf55ac53dfdc34a9b856a0bd37a2::$classMap;
62 
63         }, null, ClassLoader::class); // 这里相当于给ClassLoader类添加了一个静态成员方法
64     }
65 }

      这个类里面重点要关注getInitializer(),这个方法返回的是一个已经绑定$this对象和类作用域的闭包,利用匿名函数的绑定功能,将autoload_static类中变量的值赋给ClassLoader类中相应的私有成员变量。从而实现将自己类中的顶级命名空间映射给了 ClassLoader 类。

      5.  ClassLoader.php 自动加载核心类

      自动加载核心类对象的注册,调用核心类接口初始化顶级命名空间是在ClassLoader类里面实现的。

      由于ClassLoader代码比较多,这里只摘取相关功能实现所涉及到的部分代码来说明。具体的分析在代码注释里面:

  1 <?php
  2 namespace Composer\Autoload;
  3 class ClassLoader
  4 {
  5     // PSR-4
  6     private $prefixLengthsPsr4 = array();
  7     private $prefixDirsPsr4 = array();
  8     private $fallbackDirsPsr4 = array();
  9 
 10     // PSR-0
 11     private $prefixesPsr0 = array();
 12     private $fallbackDirsPsr0 = array();
 13 
 14     private $useIncludePath = false;
 15     private $classMap = array();
 16     private $classMapAuthoritative = false;
 17     private $missingClasses = array();
 18     private $apcuPrefix;
 19     /**
 20      * 注册自动加载核心类对象
 21      * Registers this instance as an autoloader.
 22      *
 23      * @param bool $prepend Whether to prepend the autoloader or not
 24      */
 25     public function register($prepend = false)
 26     {
 27         spl_autoload_register(array($this, 'loadClass'), true, $prepend);
 28     }
 29 
 30     /**
 31      * 自动加载的关键
 32      * Loads the given class or interface.
 33      *
 34      * @param  string    $class The name of the class
 35      * @return bool|null True if loaded, null otherwise
 36      */
 37     public function loadClass($class)
 38     {
 39         if ($file = $this->findFile($class)) {
 40             // 注意:这个用于查找路径成功后,用于加载文件的includeFile()仍然是类外面的函数
 41             includeFile($file);
 42 
 43             return true;
 44         }
 45     }
 46 
 47     /**
 48      * 在解析命名空间的时候主要分为两部分:classMap和findFileWithExtension函数
 49      * Finds the path to the file where the class is defined.
 50      *
 51      * @param string $class The name of the class
 52      *
 53      * @return string|false The path if found, false otherwise
 54      */
 55     public function findFile($class)
 56     {
 57         // class map lookup
 58         // 第一部分:直接看命名空间是否在映射数组中
 59         if (isset($this->classMap[$class])) {
 60             return $this->classMap[$class];
 61         }
 62         if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
 63             return false;
 64         }
 65         if (null !== $this->apcuPrefix) {
 66             $file = apcu_fetch($this->apcuPrefix.$class, $hit);
 67             if ($hit) {
 68                 return $file;
 69             }
 70         }
 71 
 72         // 第二部分:这个地方要麻烦一些,包含了PSRO和PSR4的实现
 73         // 首先默认用 .php 后缀名调用 findFileWithExtension 函数里,利用PSR4标准尝试解析目录文件,如果文件不存在则继续用PSR0标准解析
 74         $file = $this->findFileWithExtension($class, '.php');
 75 
 76         // 如果解析出来的目录文件仍然不存在,但是环境是 HHVM 虚拟机,继续用后缀名 .hh 再次调用 findFileWithExtension 函数
 77         // Search for Hack files if we are running on HHVM
 78         if (false === $file && defined('HHVM_VERSION')) {
 79             $file = $this->findFileWithExtension($class, '.hh');
 80         }
 81 
 82         if (null !== $this->apcuPrefix) {
 83             apcu_add($this->apcuPrefix.$class, $file);
 84         }
 85 
 86 
 87         // 如果不存在,说明此命名空间无法加载,放到missingClasses中设为true
 88         if (false === $file) {
 89             // Remember that this class does not exist.
 90             $this->missingClasses[$class] = true;
 91         }
 92 
 93         return $file;
 94     }
 95 
 96     /**
 97      * 这个方法是重点:包含了PSR0和PSR4标准的实现
 98      * @param $class
 99      * @param $ext
100      * @return false|string
101      */
102     private function findFileWithExtension($class, $ext)
103     {
104         // PSR-4 lookup
105         // 尝试利用 PSR4 标准映射目录:如果命名空间是以\开头的,要去掉\然后再匹配
106         $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
107 
108         $first = $class[0];
109         if (isset($this->prefixLengthsPsr4[$first])) {
110             $subPath = $class;
111             while (false !== $lastPos = strrpos($subPath, '\\')) {
112                 $subPath = substr($subPath, 0, $lastPos);
113                 $search = $subPath . '\\';
114                 if (isset($this->prefixDirsPsr4[$search])) {
115                     $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
116                     foreach ($this->prefixDirsPsr4[$search] as $dir) {
117                         if (file_exists($file = $dir . $pathEnd)) {
118                             return $file;
119                         }
120                     }
121                 }
122             }
123         }
124 
125         // PSR-4 fallback dirs
126         // 如果失败,则利用 fallbackDirsPsr4 数组里面的目录继续判断是否存在文件
127         foreach ($this->fallbackDirsPsr4 as $dir) {
128             if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
129                 return $file;
130             }
131         }
132 
133         // PSR-0 lookup
134         // 如果PSR4标准加载失败,则要进行PSR0标准加载
135         if (false !== $pos = strrpos($class, '\\')) {
136             // namespaced class name
137             $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
138                 . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
139         } else {
140             // PEAR-like class name
141             $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
142         }
143 
144         if (isset($this->prefixesPsr0[$first])) {
145             foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
146                 if (0 === strpos($class, $prefix)) {
147                     foreach ($dirs as $dir) {
148                         if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
149                             return $file;
150                         }
151                     }
152                 }
153             }
154         }
155 
156         // PSR-0 fallback dirs
157         // 如果失败,则利用 fallbackDirsPsr0 数组里面的目录继续判断是否存在文件
158         foreach ($this->fallbackDirsPsr0 as $dir) {
159             if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
160                 return $file;
161             }
162         }
163 
164         // PSR-0 include paths.
165         if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
166             return $file;
167         }
168 
169         return false;
170     }
171 }
172 
173 /**
174  * Scope isolated include.
175  *
176  * Prevents access to $this/self from included files.
177  */
178 function includeFile($file)
179 {
180     include $file;
181 }

  上面代码仔细阅读几遍后,发现其实并不难。主要抓住PSR4 标准和PSR0标准各自是如何映射目录的,PSR4和PSR0的区别。

  为了加深理解,这里把autoload_static类的成员属性prefixLengthsPsr4、prefixDirsPsr4、prefixesPsr0单独拿出来说明。

   注意:prefixLengthsPsr4和prefixesPsr0,都是以字母作为索引,这些字母是区分大小写的。

 1 <?php
 2 namespace Composer\Autoload;
 3 
 4 class ComposerStaticInit03d4bf55ac53dfdc34a9b856a0bd37a2
 5 {
 6     public static $prefixLengthsPsr4 = array (
 7         'Q' =>array (
 8             'Qcloud\\Sms\\' => 11,
 9         ),
10         'P' =>
11         array (
12             'Psy\\' => 4,
13             'Psr\\SimpleCache\\' => 16,
14             'Psr\\Log\\' => 8,
15             'Psr\\Http\\Message\\' => 17,
16             'Psr\\Http\\Client\\' => 16,
17             'Psr\\Container\\' => 14,
18             'Prophecy\\' => 9,
19             'Predis\\' => 7,
20             'PhpParser\\' => 10,
21             'PhpOffice\\PhpSpreadsheet\\' => 25,
22         )
23     );
24 
25     public static $prefixDirsPsr4 = array (
26         'phpDocumentor\\Reflection\\' =>
27         array (
28             0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',
29             1 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src',
30             2 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',
31         ),
32         'ZipStream\\' =>
33         array (
34             0 => __DIR__ . '/..' . '/maennchen/zipstream-php/src',
35         )
36     );
37 
38     public static $prefixesPsr0 = array (
39         'U' =>
40         array (
41             'UpdateHelper\\' =>
42             array (
43                 0 => __DIR__ . '/..' . '/kylekatarnls/update-helper/src',
44             ),
45         ),
46         'G' => 
47         array (
48             'Guzzle\\Tests' => 
49             array (
50                 0 => __DIR__ . '/..' . '/guzzle/guzzle/tests',
51             ),
52             'Guzzle' => 
53             array (
54                 0 => __DIR__ . '/..' . '/guzzle/guzzle/src',
55             ),
56         )
57     );
58 }

    由于整体描述起来比较复杂,建议通过仔细阅读源码,以及用debug多调试几次,来熟悉这个具体实现过程。

    这里强调其中的几个重点:

   1)PSR4标准顶级命名空间映射数组:分为$prefixLengthsPsr4和$prefixDirsPsr4,PSR4 标准是用顶级命名空间目录替换顶级命名空间。

   2)利用PSR4标准映射目录的流程:比如我们要找$class为phpDocumentor\Reflection\Element()的命名空间,首先就会取第一个字母p,到prefixLengthsPsr4中查找是否有记录,如果有,

  再到$prefixDirsPsr4中去查找phpDocumentor\Reflection\\对应的索引数组,如果有,就将这个数组遍历,然后以这个值作为目录$dir,Element.php作为$pathEnd,将$dir与$pathEnd拼接起来作为一个文件,然后去查找这个文件是否存在,如果存在就返回该文件。这里只是描述了一个大体流程,具体细节比如某些地方针对'\'的处理还要仔细阅读源码。

  3)利用PSR0标准映射目录的流程:还是以上面的例子,要找phpDocumentor\Reflection\Element(),首先就会取第一个字母p,到prefixesPsr0中查找是否有记录,如果有,就将对应的数组遍历,这个$k比如为phpDocumentor\\Reflection,如果$class中存在这个$k,那么就继续遍历对应的索引数组$v,如果根据$dir . DIRECTORY_SEPARATOR . $logicalPathPsr0拼装的文件存在,则返回这个文件。具体细节比如某些地方针对'_'的处理还要仔细阅读源码。

  总结

  

   最后,总结下composer中各个文件的作用,根据作用依次顺序为:

    1. autoload.php  不做具体逻辑,引入自动加载引导类autoload_real.php,调用引导类中getLoader方法

    2. autoload_real.php    自动加载核心类对象的初始化和注册

    3. autoload_static.php  自动加载核心类对象的顶级命名空间静态初始化(通过匿名绑定函数实现)

    4. ClassLoader.php   自动加载核心类,其实所有功能的实现都是围绕着这个类来展开

    5. autoload_files.php   用于加载全局函数的文件,返回值为数组,key为哈希值,value为文件路径

    普通初始化需要用到的文件:

    6. autoload_namespaces.php :返回一个数组,存放着顶级命名空间与文件的映射(符合PSR0 标准)

    7. autoload_psr4.php:返回一个数组,存放着顶级命名空间与文件的映射(符合PSR4 标准)

    8. autoload_classmap.php: 返回一个数组,存放着有完整的命名空间和文件目录的映射(自动加载的最简单形式)

   

   疑问 

   到这里,composer的源码分析就基本结束了,虽然还是有些疑问,比如:

   1)根据apcu前缀来查找文件,这个是拿来干嘛的?

   2)为什么对于自动加载命名空间和全局函数都要分为两种方式?

   3)使用静态初始化的条件为什么跟PHP版本小于5.6、HHVM虚拟机环境、存在zend_loader_file_encoded有关系?

 

   不过这也不影响我们对composer整理流程实现的分析。 熟悉了composer的实现原理,相信我们对于PHP的SPL自动加载机制以及PSR规范的理解和认知方面,又上了一个新的台阶!

   任何事物的发展都是有一个循序渐进的过程,学习亦是如此!相信通过长期的积累和探索,等到某一天,再回过头来,又可以尝试着对这篇文章里提到的疑问进行解答!

 

参考链接:

https://segmentfault.com/a/1190000014948542
https://zyf.im/2019/04/28/composer-autoload-in-laravel/
https://segmentfault.com/q/1010000007149666
https://www.laruence.com/2012/09/12/2765.html

posted @ 2021-01-19 11:19  欢乐豆123  阅读(237)  评论(0编辑  收藏  举报