Laravel 框架如何基于 Composer 实现类和文件的自动加载
对于熟练使用 Laravel 框架的同学来说,对 Composer 肯定不陌生,我们在 Laravel 框架中无论使用辅助函数还是 PHP 类,都不会涉及到类和文件的加载操作,但实际上这个工作是 Composer 这个大管家在底层默默帮我们完成的,那么,有多少同学知道 Composer 底层是如何实现 PHP 命名空间与文件目录的映射,以及类和文件的自动加载的?
以 Laravel 项目为例,在 public/index.php
中可以看到应用在一开始就引入了类加载器:
require __DIR__.'/../vendor/autoload.php';
对应的 vendor/autoload.php
文件由 Composer 在初始化的时候生成,代码很简单:
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitXXX::getLoader();
注:
ComposerAutoloaderInitXXX
中的XXX
表示用于标识不同项目的唯一标识符,比如我这里是db98a94c5e37268893638e22c0a1dd83
,为了方便描述,我们后续都用XXX
来指代它。
接下来我们来重点关注 vendor/composer/autoload_real.php
文件中 ComposerAutoloaderInitXXX::getLoader()
的实现代码:
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInitXXX', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInitXXX', 'loadClassLoader'));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require_once __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInitXXX::getInitializer($loader));
} else {
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
}
$loader->register(true);
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInitXXX::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequireXXX($fileIdentifier, $file);
}
return $loader;
}
在 ComposerAutoloaderInitXXX
类中,使用了一个私有的静态属性 $loader
用于持有 Composer\Autoload\ClassLoader
实例,在一次 HTTP 请求处理过程中,如果类加载器已经初始化过,则直接返回 $loader
,以提高应用性能。否则的话才继续往下走。
接下来,通过 PHP 内置函数 spl_autoload_register
注册类加载器,当应用代码中遇到未定义类时会通过 ComposerAutoloaderInitXXX
的 loadClassLoader
方法加载该类,然后我们看下一行代码:
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
这里会对静态属性 $loader
进行赋值,对应的值是 \Composer\Autoload\ClassLoader
类实例,这里就需要用到我们上一行注册的类加载器对 \Composer\Autoload\ClassLoader
类进行加载,通过查看源码我们可以得出 Composer 会从 vendor/composer/ClassLoader.php
中查找该类的结论。
紧接着在 ComposerAutoloaderInitXXX::getLoader()
方法中会通过 spl_autoload_unregister
函数注销之前注册的类加载器。
再往后就是 Composer 类加载机制的核心逻辑了,从 5.6 版本开始 PHP 支持静态初始化,所以做了区别处理,对于满足静态初始化条件的,引入 vendor/composer/autoload_static.php
文件,并调用 \Composer\Autoload\ComposerStaticInitXXX::getInitializer($loader)
方法设置 $loader
实例的 prefixLengthsPsr4
、prefixDirsPsr4
、prefixesPsr0
、classMap
属性。否则的话,引入 vendor/composer/autoload_namespaces.php
文件,调用 $loader->set($namespace, $path);
方法设置 prefixesPsr0
属性,然后引入 vendor/composer/autoload_namespaces.php
文件,调用 $loader->setPsr4($namespace, $path);
方法设置 prefixLengthsPsr4
和 prefixDirsPsr4
属性,最后引入 vendor/composer/autoload_classmap.php
文件,调用 $loader->addClassMap($classMap);
方法设置 classMap
属性。
上述条件分支语句处理的最终结果是一致的,都是完成 $loader
实例 prefixLengthsPsr4
、prefixDirsPsr4
、prefixesPsr0
、classMap
属性的初始化,其中 classMap
管理的是完整类名(可能包含命名空间)与文件路径的映射关系、prefixDirsPsr4
管理的是命名空间与文件目录的映射关系(遵循 psr-4 规范)、prefixLengthsPsr4
管理的是命名空间前缀长度、prefixesPsr0
管理也是命名空间与文件目录映射关系(遵循 psr-0 规范,极少数历史包在使用,目前 psr-0 规范已废弃)。这些映射关系都是运行 Composer 安装或更新命令时,系统自动帮我们维护的(从每个依赖包和项目根目录下的 comspoer.json
读取 autoload
配置实现),既包含位于 vendor
目录下依赖包的类和命名空间,也包含项目根目录下用户自定义的类和命名空间,对于用户自定义的命名空间,需要在 composer.json
的 autoload
配置项中显式配置,才能被系统识别(实际上,每个依赖包也是这么做的,感兴趣的话可以去看一下):
"autoload": {
"psr-4": {
"App\\": "app/"
},
"classmap": [
"database/seeds",
"database/factories"
]
},
接下来,调用 $loader->register(true);
完成 Composer 包管理器类自动加载注册:
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
还是调用 spl_autoload_register
来完成,这是这次类自动加载处理函数变成了 $loader
实例的 loadClass
方法,当 Laravel 应用执行过程中遇到未定义类时,会调用该方法,然后通过传入的类名调用 $loader->findFile($class)
方法查找对应类所在文件路径并引入:
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
如果 $loader->classMap
属性中已经包含该类则直接返回对应文件路径,否则继续往后执行,如果该类别标识为缺失则不再往下执行,如果启用了 apcu
扩展,则先从对应缓存中获取文件路径,接下来开始常规查找,调用 $this->findFileWithExtension($class, '.php');
方法先通过 PSR-4 规范查找类对应文件路径,如果找到则返回,否则再通过 PSR-0 规范查找,这其中会用到 $loader
初始化时设置的 prefixLengthsPsr4
、prefixDirsPsr4
、prefixesPsr0
属性来组合出查找条件。如果最终没有找到,则返回 false
。回到 findFile
方法,如果返回值为 false,并且定义了 HHVM_VERSION
常量,则将文件后缀改为 .hh
再找一次。如果最后文件路径没有找到,则将其添加到 $loader->missingClasses
属性中,如果启用了 apcu
扩展,不管找没找到,都将结果保存到 apcu
缓存。
如果类对应的文件路径最终没有找到,会抛出异常(spl_autoload_register
方法第二个参数为 true
表示未找到类时抛出异常)。
下面我们回到 ComposerAutoloaderInitXXX::getLoader()
方法,继续往下面看,在 Laravel 项目中,除了类之外,还支持不归属于任何类的辅助函数,这些辅助函数通常定义在 helpers.php
文件中,Composer 通用支持对这类文件的自动加载,这一块的处理通样针对是否支持静态初始化进行了区分,如果支持静态初始化,则通过 Composer\Autoload\ComposerStaticInitXXX::$files;
返回自动加载的文件数组,否则的话引入 vendor/composer/autoload_files.php
返回需要自动加载的文件数组。最后遍历这些文件逐个引入,并将它们的标识符存放到全局变量 __composer_autoload_files
中。
同样这些自动加载的文件数组由 Composer 帮我们维护,对于通过 Composer 安装的依赖包,Composer 会读取每个依赖包的 composer.json
中的 autoload.files
配置项获取自动加载文件,并维护到 vendor/composer/autoload_files.php
和 vendor/composer/autoload_static.php
数组中,对于项目根目录下用户自定义的 PHP 文件,需要在 composer.json
的 autoload
中维护到 files
数组配置项:
"autoload": {
...
"files": [
"app/helpers.php"
]
对于通过 Composer 安装的依赖包,执行安装命令时 Composer 会自动维护命名空间、类和文件的映射,对于在项目根目录下由开发者自定义的命名空间、类和文件,需要在新增后手动运行 composer dump-auto
命令将其更新到 vendor/composer
目录下相应的自动加载配置中,否则系统将无法找到对应的类和文件。
对应 Composer 如何管理命名空间和文件目录的映射关系,以及如何实现类和文件的自动加载,你明白了吗?
总结下来,Composer 支持四种自动加载方式,对应在 composer.json
中,即 autoload
配置项中的 classmap
、psr-4
、psr-0
和 files
四个配置,前三个都是维护类的自动加载,最后一个维护的是文件的自动加载,其中 classmap
通常用来管理不归属于命名空间的类,比如 databases
目录下的类,在配置的时候只需要配置目录路径即可,而 psr-4
通常用来管理归属于命名空间的类,我们在配置的时候只需配置根命名空间与对应目录的映射即可,psr-0
已经废弃,很少用到,对应 files
,我们需要配置完整的文件路径。
2 Comments
赞,收货颇多
请问一下,我看到autoload_classmap.php中有'App\Console\Kernel' => $baseDir . '/app/Console/Kernel.php', 但是我从根项目的composer.json文件中,并没有看到相关的classmap的配置,请问laravel是怎么解析生成'App\Console\Kernel' => $baseDir . '/app/Console/Kernel.php'的?