前言
自动加载文件是一个框架的核心,在很久之前没有引入 composer
包管理之前,引入代码文件都是直接通过 require
和 include
的方式,在项目很小的时候,问题不是很大,但是对于一个庞大的项目来说,这种引入方式,使得代码结构混乱不堪,难以维护,基于Psr规范的 composer
很好的解决了这个问题,下面基于 Laravel
框架的源码分析 Composer
自动加载的原理。
Composer 源码分析
启动
define('LARAVEL_START', microtime(true));
require __DIR__.'/../vendor/autoload.php';
引入 Vendor
目录下面的 autoload.php
文件
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit0ead93d159fabd8f373a57d2b2e3ecd9::getLoader();
Composer
自动加载真正开始的地方
composer 自动加载文件
首先先了解一下 Composer
自动加载所需要到的源文件
1、autoload_real.php 自动加载的引导类
2、ClassLoader.php 自动加载的核心类
3、autoload_static.php 顶级命名空间初始化类
4、autoload_classmap.php 有完整的命名空间和文件目录的映射
5、autoload_files.php 用于加载全局加载函数的文件,存放各个全局函数所在的文件路径名
6、autoload_namespaces.php 符合PSR0标准的自动加载文件,存放着顶级命名空间和文件的映射
7、autoload_psr4.php 符合PSR4标准的自动加载文件,存放着顶级命名空间和文件的映射
核心函数getLoader
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInit0ead93d159fabd8f373a57d2b2e3ecd9', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit0ead93d159fabd8f373a57d2b2e3ecd9', '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\ComposerStaticInit0ead93d159fabd8f373a57d2b2e3ecd9::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\ComposerStaticInit0ead93d159fabd8f373a57d2b2e3ecd9::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire0ead93d159fabd8f373a57d2b2e3ecd9($fileIdentifier, $file);
}
return $loader;
}
上述代码,是加载的核心过程,我将分为几部分开始分析。
单例
这个部分很简单,就是最经典的单例模式,只能有一个
if (null !== self::$loader) {
return self::$loader;
}
构造ClassLoader核心类
spl_autoload_register(array('ComposerAutoloaderInit0ead93d159fabd8f373a57d2b2e3ecd9', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit0ead93d159fabd8f373a57d2b2e3ecd9', 'loadClassLoader'));
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
上述代码也不难理解,先注册了一个自动加载函数,然后通过自动加载函数引入核心加载类 ClassLoader
,再通过 new
生产一个对象,最后销毁这个自动加载函数。
初始化核心类对象
$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\ComposerStaticInit0ead93d159fabd8f373a57d2b2e3ecd9::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);
}
}
这一部分类就是对自动加载类的初始化,主要给自动加载核心类初始化顶级命名空间映射。
主要是通过两种方式:
- 使用
autoload_static.php
文件直接静态初始化 - 调用核心类接口初始化
使用autoload_static静态文件初始化
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit0ead93d159fabd8f373a57d2b2e3ecd9::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit0ead93d159fabd8f373a57d2b2e3ecd9::$prefixDirsPsr4;
$loader->prefixesPsr0 = ComposerStaticInit0ead93d159fabd8f373a57d2b2e3ecd9::$prefixesPsr0;
$loader->classMap = ComposerStaticInit0ead93d159fabd8f373a57d2b2e3ecd9::$classMap;
}, null, ClassLoader::class);
}
我们可以看到直接引入了一个文件 autoload_static.php
,然后调用 getInitializer
方法初始化,将类中的顶级命名空间映射给了 classLoader
类,值得注意的是返回的是一个匿名函数,为什么呢?原因就是 classLoader
类中的 $prefixLengthsPsr4
等变量都是 private
的,利用匿名函数绑定功能就可以将这些 private
变量赋给 classLoader
类里的成员变量。
接下来就是命名空间初始化的关键:
- classMap (命名空间映射)
public static $classMap = array (
'App\\Console\\Kernel' => __DIR__ . '/../..' . '/app/Console/Kernel.php',
'App\\Exceptions\\Handler' => __DIR__ . '/../..' . '/app/Exceptions/Handler.php',
'App\\Http\\Controllers\\Auth\\ForgotPasswordController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/ForgotPasswordController.php',
'App\\Http\\Controllers\\Auth\\LoginController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/LoginController.php',
......
)
直接命名空间全名与目录的映射,简单粗暴,也会导致整个数组非常的大。
- PSR4 标准顶级命名空间映射数组
public static $prefixLengthsPsr4 = array (
'p' =>
array (
'phpDocumentor\\Reflection\\' => 25,
),
......
)
public static $prefixDirsPsr4 = array (
'phpDocumentor\\Reflection\\' =>
array (
0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',
1 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src',
2 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',
),
......
)
PSR4 标准顶级命名空间映射用了两个数组,一个是用命名空间第一个字母作为前缀,然后是顶级命名空间,但是最终并不是文件路径,而是顶级命名空间的长度,为什么呢?
这是因为PSR4标准是用顶级命名空间目录替换顶级命名空间,所以获得顶级命名空间的长度很重要。
ClassLoader 接口初始化
如果 PHP
版本低于5.6或者使用HHVM虚拟环境,那么就要使用核心类的接口进行初始化。
//PSR0 标准
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
//PSR4 标准
$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);
}
自动加载核心类ClassLoader的静态初始化到这里就完成了!
其实自动加载真正重要的就是两部分,初始化和注册。初始化负责顶层命名空间的目录映射,注册负责实现顶层以下的命名空间映射规则。
注册
上述内容分析完成了 Composer
自动加载功能的启动和初始化,经过启动和初始化,自动加载核心对象已经获得了顶级命名空间与相应目录的映射,也就是说,如果有命名空间 App\Console\Kernel
,我们已经找到它对应的类文件所在位置。那么,它是什么时候触发去找的呢?
这个就是 Composer
自动加载的核心了,我们来看看核心类的 register()
函数
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
其实奥秘都在自动加载类 ClassLoader
的 LoadClass
的函数上:
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
每当 PHP
遇到一个不认识的命名空间的时候,PHP都会自动调用 spl_autoload_register
里面的 loadClass()
函数,然后找到命名空间对应的文件。
全局函数的自动加载
Composer
不止可以自动加载命名空间,还可以加载全局函数,怎么实现的呢?把全局函数写到特定的文件,在程序运行前依次 require
就行了,这个就是 composer
自动加载的第五步,加载全局函数。
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInit0ead93d159fabd8f373a57d2b2e3ecd9::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire0ead93d159fabd8f373a57d2b2e3ecd9($fileIdentifier, $file);
}
加载全局函数和 Composer
初始化一样,分为两种:静态初始化和普通初始化,静态加载只支持PHP5.6以上并且不支持HHVM.
静态初始化
ComposerStaticInit0ead93d159fabd8f373a57d2b2e3ecd9
:
public static $files = array (
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
......
)
普通初始化
autoload_files.php
return array(
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
......
)
加载全局函数
function composerRequire0ead93d159fabd8f373a57d2b2e3ecd9($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}
运行
到这里终于来到了自动加载核心的核心,命名空间如何通过 composer
转为对应目录文件的奥秘就在这里。
看下 loadClass()
函数:
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
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;
}
我们看到 loadClass()
,主要调用 findFile()
函数。findFile()
在解析命名空间的时候主要分为两部分:classMap
和 findFileWithExtension()
函数。classMap
很简单,直接看命名空间是否在映射数组中即可。麻烦的是 findFileWithExtension()
函数,这个函数包含了 PSR0
和 PSR4
标准的实现。还有个值得我们注意的是查找路径成功后 includeFile()
仍然是外面的函数,并不是 ClassLoader
的成员函数,原理跟上面一样,防止有用户写 $this
或 self
。还有就是如果命名空间是以\开头的,要去掉\然后再匹配。
最后看下 findFileWithExtension()
函数
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
举例
文章的最后,我们通过一个例子来说明一下上述代码的执行流程:
如果我们在代码中写下 new phpDocumentor\Reflection\Element()
,PHP 会通过 SPL_autoload_register
调用 loadClass -> findFile -> findFileWithExtension
。步骤如下:
- 将 \ 转为文件分隔符/,加上后缀
php
,变成$logicalPathPsr4
, 即phpDocumentor/Reflection//Element.php;
- 利用命名空间第一个字母p作为前缀索引搜索
prefixLengthsPsr4
数组,查到下面这个数组:
p' =>
array (
'phpDocumentor\\Reflection\\' => 25,
'phpDocumentor\\Fake\\' => 19,
)
- 遍历这个数组,得到两个顶层命名空间
phpDocumentor\Reflection\
和phpDocumentor\Fake\
- 在这个数组中查找
phpDocumentor\Reflection\Element
,找出phpDocumentor\Reflection\
这个顶层命名空间并且长度为25。 - 在
prefixDirsPsr4
映射数组中得到phpDocumentor\Reflection\
的目录映射为:
'phpDocumentor\\Reflection\\' =>
array (
0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',
1 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',
2 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src',
),
- 遍历这个映射数组,得到三个目录映射;
- 查看 “目录+文件分隔符//+substr($logicalPathPsr4, $length)”文件是否存在,存在即返回。这里就是
‘DIR/…/phpdocumentor/reflection-common/src + substr(phpDocumentor/Reflection/Element.php,25)’ - 如果失败,则利用 fallbackDirsPsr4 数组里面的目录继续判断是否存在文件
以上就是 Composer
自动加载的原理解析!
本文部分内容参考:https://segmentfault.com/a/1190000014948542