composer 自动加载原理分析
2022-01-31 本文已影响0人
sorry510
composer install 或 update
生成一个
vender
目录,结构如下
| vender
--| composer
----| autoload_classmap.php
----| autoload_files.php
----| autoload_namespaces.php
----| autoload_psr4.php
----| autoload_real.php
----| autoload_static.php
----| ClassLoader.php
----| installed.json
----| installed.php
----| InstalledVersions.php
----| LICENSE
----| platform_check.php
| autoload.php // 入口文件
使用自动加载
// test.php 在根目录下
define('BASE_PATH', dirname(__FILE__));
$loader = require BASE_PATH . './vendor/autoload.php';
自动加载原理分析
1. 首先执行 autoload_real.php
文件中的类(类名为随机生成为了保证唯一性)的静态方法 getLoader
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInited8b61ae552a5f53651efbac4ac65b0c', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
spl_autoload_unregister(array('ComposerAutoloaderInited8b61ae552a5f53651efbac4ac65b0c', 'loadClassLoader'));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInited8b61ae552a5f53651efbac4ac65b0c::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\ComposerStaticInited8b61ae552a5f53651efbac4ac65b0c::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequireed8b61ae552a5f53651efbac4ac65b0c($fileIdentifier, $file);
}
return $loader;
}
2. 在执行getLoader
方法的过程中,首先执行 platform_check.php
检查当前环境是否满足条件
require __DIR__ . '/platform_check.php';
3. 然后执行 spl_autoload_register(不懂这个方法可以点击看官方文档) 方法注册当前类的 loadClassLoader
方法为自动加载的回调函数
spl_autoload_register(array('ComposerAutoloaderInited8b61ae552a5f53651efbac4ac65b0c', 'loadClassLoader'), true, true);
当执行下一行代码 $loader = new \Composer\Autoload\ClassLoader
时会触发 loadClassLoader
方法,然后会引入 ClassLoader.php
文件, 如下代码
// autoload_real.php
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
之后再取消这个注册函数,避免真正使用自动加载时受其影响
spl_autoload_unregister(array('ComposerAutoloaderInited8b61ae552a5f53651efbac4ac65b0c', 'loadClassLoader'));
4. 根据 $useStaticLoader
判断是否使用静态加载方式,通常为true,然后会引入 autoload_static.php
文件,此文件记录了各个扩展包和根目录中 composer.json
中 autoload
属性值psr-4,files,classmap
的对应关系, 分为 psr4
, classmap
和 files
3 类, 然后执行 call_user_func
调用autoload_static.php
文件中类的静态方法 getInitializer
,将这些对应关系绑定到 $loader
对象上,代码如下
// autoload_real.php
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInited8b61ae552a5f53651efbac4ac65b0c::getInitializer($loader));
}
// autoload_static.php
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInited8b61ae552a5f53651efbac4ac65b0c::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInited8b61ae552a5f53651efbac4ac65b0c::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInited8b61ae552a5f53651efbac4ac65b0c::$classMap;
}, null, ClassLoader::class);
}
autoload 属性的数据结构
- files 格式为数组,记录所有
files
对应的文件
key 为唯一 hash id
value 为文件路径
[
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
]
- classmap 格式为数组,记录当前项目(composer dump 之后的文件变动不算)
classmap
文件夹中所有文件和psr-4
对应目录下的所有文件
key 为类的完整命名
value 为文件路径
[
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
]
- psr4 分为2个,记录所有
psr-4
对应属性, 一个按照首字母进行分组,一个记录所有的命名空间映射
$prefixLengthsPsr4
key 为命名空间首字母
value 为数组记录每个命名空间的长度
[
'S' =>
[
'Symfony\\Polyfill\\Php80\\' => 23,
'Symfony\\Polyfill\\Mbstring\\' => 26,
'Symfony\\Component\\VarDumper\\' => 28,
'Simple\\Test\\' => 12,
]
]
loader对象的
setPsr4
方法重新设置或addPsr4
方法进行顺序调整。
[
'Symfony\\Polyfill\\Php80\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
),
]
5. 之后执行 $loader->register(true)
,开始进行真正的自动加载函数注册
// ClassLoader.php
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
// ClassLoader.php
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
return null;
}
其中关键为
fileFile
方法,findFile
分为 2 类,一类是classmap
已经提前收集到文件,这样可以以 O(1) 的速度找到对应文件, 另一类是尚未被composer dump -o
的符合psr4
自动加载规范的文件, 会执行findFileWithExtension
方法寻找文件所在path
// ClassLoader.php
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;
}
// ClassLoader.php
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;
}
}
return false;
}
ps:
$this->fallbackDirsPsr4
是用于不符合psr4
加载规范的文件命名例如 Test.php 文件的命名空间namespace Foo\Test
, 文件路径为Bar/Foo/Test.php
,可以使用$loader
的addPsr4
方法进行添加, $loader->addPsr4(null, BASE_PATH . './Bar');
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
6. 之后将所有 composer.json
文件中 autoload
属性中的 files
所记录的文件依次 require
,
// autoload_real.php
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInited8b61ae552a5f53651efbac4ac65b0c::$files; // 使用 composer dump 缓存后的文件
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequireed8b61ae552a5f53651efbac4ac65b0c($fileIdentifier, $file);
}
function composerRequireed8b61ae552a5f53651efbac4ac65b0c($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}