Laravel 源码阅读之一:Composer 自动加载分析
代码堆栈:入口文件
public/index.php
←vendor/autoload.php
←vendor/composer/autoload_real.php
←ComposerAutoloaderInitxxx::getLoader()
(因原类名较长,类名后面的hash字串以‘xxx’代替)
自动加载的初始化和自动加载函数的注册都将在getLoader
方法中实现。loadClassLoader方法
//该方法用于自动加载ClassLoader类 public static function loadClassLoader($class) { if ('Composer\Autoload\ClassLoader' === $class) { require __DIR__ . '/ClassLoader.php'; } }
getLoader方法代码及分析
public static function getLoader()
{
if (null !== self::$loader) { // 检查$loaders是否有值,有则直接返回
return self::$loader;
}
/*
|---------------------------------------------------------
| 将 `ComposerAutoloaderInitxxx` 类的`loadClassLoader`方法注册为一个
| `__autoload`函数的实现,无法注册成功则抛出错误,且添加到自动加载函数队
| 列前面(即使用的类找不到时,自动调用`loadClassLoader`方法实现自动加载,
| 具体实现见后面该方法分析)
|---------------------------------------------------------
*/
spl_autoload_register(array('ComposerAutoloaderInitxxx', 'loadClassLoader'), true, true);
/*
|---------------------------------------------------------
| 这里实例化一个ClassLoader类,并赋值到$loader成员。
| \Composer\Autoload\ClassLoader()按照字面的路径是找不到该类的,
| 所以会触发`loadClassLoader`方法实现自动加载。
|---------------------------------------------------------
*/
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
// 得到 $loader 之后去掉前面注册的自动加载实现
spl_autoload_unregister(array(ComposerAutoloaderInitxxx', 'loadClassLoader'));
// 静态初始化只支持 PHP5.6 以上版本并且不支持 HHVM 虚拟机
// zend_loader_file_encoded不清楚是什么,我的版本$useStaticLoader=true
$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';
//调用getInitializer方法
// getInitializer见后面分析(B)
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);
}
}
//register方法将classLoader方法加入自动加载函数队列
//只要程序遇到不认识的类,就会使用该队列中的函数去查找类对应的文件
//最后require进来,查找不到会做一个标记,下次查找时就可以直接识别该类
//的文件是找不到的,直接返回false。后面展开分析该函数(C)
$loader->register(true);
//加载全局函数(分静态加载和非静态加载,结果是一样的)
//$files成员变量是一个数组,包含'文件标识(哈希值)=>文件路径'的键值对
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInitxxx::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
//composerRequirexxx在类的外部,封装了require函数,这样可以实现作用域隔离,
//require进来的文件里面的变量,其作用域被包裹在`composerRequirexxx`中,
//防止require进来的文件含有$this或self而产生调用混淆或错误,
//而且该函数实现了require_once的效果,效率更高。分析见(A)部分。
composerRequirexxx($fileIdentifier, $file);
}
//到这里,该方法返回一个`ClassLoader`类的实例,该实例的成员变量
//(prefixLengthsPsr4、prefixDirsPsr4、fallbackDirsPsr4、prefixesPsr0等
//都已经初始化,并且将`classLoader`方法添加到自动加载函数队列)
return $loader;
}
loadClassLoader方法如何实现自动加载
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
当程序执行到self::$loader = $loader = new \Composer\Autoload\ClassLoader();
时,\Composer\Autoload\ClassLoader
类并不能找到,这将去查找 __autoload
的实现函数队列,最终找到loadClassLoader
方法可以实现类的加载,并将 \Composer\Autoload\ClassLoade
作为参数转入。所以上面代码中,if
条件成立,将 __DIR__ . '/ClassLoader.php'
文件导入。
(A) composerRequirexxx方法分析
function composerRequirexxx($fileIdentifier, $file)
{
//文件标识为空才加载文件,实现了require_once的效果
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
//`$file`里面的变量,其作用域被包裹在 `composerRequirexxx`
require $file;
//将文件标识为已加载过的
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}
(B) getInitializer方法分析
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::$prefixDirsPsr4;
$loader->fallbackDirsPsr4 = ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::$fallbackDirsPsr4;
$loader->prefixesPsr0 = ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::$prefixesPsr0;
$loader->classMap = ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::$classMap;
}, null, ClassLoader::class);
}
Closure绑定的用法可参考:博客:PHP 闭包(Closure)
这里将一个闭包绑定到ClassLoader
类,使得该类的私有成员变量可以被赋值,从而将这里的变量(顶级空间命名映射)搬到该类中。
该函数执行后得到的结果:ClassLoader
的成员变量实现了初始化,即它们保存了顶级命名空间到文件夹路径的映射。
(C) register方法分析
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
该方法将loadClass
方法加入自动加载函数队列。loadClass
方法:
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
这里主要看findFile
方法是怎么找到file的。接下来看findFile
方法。
findFile
方法分析
public function findFile($class)
{
// class map lookup
//如果classMap中有该类的文件映射,则直接返回对应的文件
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
// isset($this->missingClasses[$class]表示
//如果这个类已经被标为找不到的,则直接返回false
//$this->classMapAuthoritative暂不清楚作用
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
//暂不清楚是啥
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
//使用psr4、psr0标准查找
$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;
}
findFileWithExtension
方法
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
//将‘\’转为‘/’并加上后缀
//以下分析,假设$class = 'phpDocumentor\Reflection\example'
//将'phpDocumentor\Reflection\example' 转为 'phpDocumentor/Reflection/example.php'
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
//取第一个字母‘p’
$first = $class[0];
//prefixLengthsPsr4数组中,有'P' => [ 'phpDocumentor\Reflection\' => 25 ]
//这时,该条件为true(php数组key不区分大小写)
//将命名空间用首字母归类,可以实现快速查找,如,这里如果没有找到‘P’作为开头的,
//就可以不用继续查找,而是换别的查找方法。
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
//计算字符串中最后一个‘\’的位置,并赋值给$lastPos,并判断是否存在‘\’
//$lastPos = 24
while (false !== $lastPos = strrpos($subPath, '\\')) {
//从字符串开头算起,取$lastPos个字符
//这里得到$subPath='phpDocumentor\Reflection'
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath.'\\';
//查找prefixDirsPsr4数组对应key是否有值,其key-value值如下:
/*
'phpDocumentor\Reflection\'=>
['/home/vagrant/code/Laravel/vendor/composer/../phpdocumentor/reflection-common/src',
'/home/vagrant/code/Laravel/vendor/composer/../phpdocumentor/reflection-docblock/src',
'/home/vagrant/code/Laravel/vendor/composer/../phpdocumentor/type-resolver/src']
*/
if (isset($this->prefixDirsPsr4[$search])) {
//$pathEnd = '/example.php'
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
//逐个检查prefixDirsPsr4['phpDocumentor\Reflection\']的文件路径是否包含需要的文件
//例如,检查/home/vagrant/code/Laravel/vendor/composer/../phpdocumentor/reflection-common/src/example.php文件是否存在
//如果存在,则返回文件路径
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;
}
探索和疑问
call_user_func
是运行时才确定要调用的函数是什么,一般在不确定需要调用的函数是哪个的情况下使用,传入一个变量函数,该变量会根据程序的条件不同而不同(参考),效率比直接调用函数低。尝试将autoload_real.php
中的这一行 :call_user_func(\Composer\Autoload\ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::getInitializer($loader));
改成:\Composer\Autoload\ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::getInitializer($loader);
,结果程序执行到getInitializer
的return行直接跳出,$loader
的成员变量并没有得到初始化,不知道为何会跟使用call_user_fun不一样。-
getInitializer
可以改写成:public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { $this->prefixLengthsPsr4 = ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::$prefixLengthsPsr4; $this->prefixDirsPsr4 = ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::$prefixDirsPsr4; $this->fallbackDirsPsr4 = ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::$fallbackDirsPsr4; $this->prefixesPsr0 = ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::$prefixesPsr0; $this->classMap = ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::$classMap; }, $loader, ClassLoader::class); }
这里将闭包函数的
$this
绑定到ClassLoader
类的实例$loader
。 - 全局函数在自动加载的时候全部加载进来,如果全局函数的数量较多,可能会对程序性能造成影响,所以,对于一些需要用到的函数,使用PHP 的
Trait
的来引入比较好。
小结
Composer自动加载所完成的工作有:
- 实例化
ClassLoader
类,并初始化其成员变量,将顶级命名空间映射到文件夹路径的映射保存其中 - 将
loadClass
方法加入自动加载函数队列,且该方法实现了classMap,psr4,psr0等方式的文件路径查找。当程序遇到不认识的类时,会调用该方法进行文件的加载 - 实现全局函数的加载
总的来说,Composer自动加载一方面接管了我们手动写一堆require或include的工作(想像一下,要require或include几千个文件会是什么样的情形),大大提高了开发效率和简洁代码;另一方面,Composer自动加载是使用到了类的时候才去查找并加载类的文件,实现了按需加载,节约程序开销,提高了程序的性能。
本作品采用《CC 协议》,转载必须注明作者和本文链接
因为上述代码只是执行
getInitializer
方法,从,并没有执行getInitializer返回的匿名方法
。按照你的思路,应该改成
\Composer\Autoload\ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::getInitializer($loader)();