自动加载(composer分析)
这一章详细讲解入口文件第二行代码发生了什么,首先上代码:
require __DIR__ . '/../vendor/autoload.php';
接下来开始具体分析:
__DIR__
是PHP的内置常量,意思是获取当前文件所在位置,当前文件是入口文件index.php
,然后获取到当前文档之后用../
来切换目录到当前文件的上级目录,然后找到vendor
目录下的autoload.php
文件。
下面其实是composer的加载机制:
1、autoload.php开始
上autoload.php
的代码:
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit1a384f2130f559ec4821f173d482452a::getLoader();
由第一行的注释来看,此文件是Composer生成的,第二行又是一个引入,第三行是执行引入的这个类的一个静态方法getLoader()
,我们找到autoload_real.php
,首先上完整代码:
class ComposerAutoloaderInit1a384f2130f559ec4821f173d482452a
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInit1a384f2130f559ec4821f173d482452a', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit1a384f2130f559ec4821f173d482452a', '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\ComposerStaticInit1a384f2130f559ec4821f173d482452a::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\ComposerStaticInit1a384f2130f559ec4821f173d482452a::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire1a384f2130f559ec4821f173d482452a($fileIdentifier, $file);
}
return $loader;
}
}
function composerRequire1a384f2130f559ec4821f173d482452a($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}
2、单例
我们一点一点分析这个类,因为autoload.php
中调用了getLoader()
方法,所以首先找到getLoader()
方法,看这个方法里的第一部分:
if (null !== self::$loader) {
return self::$loader;
}
可以看到默认是从类的静态属性$loader
里取所有的加载项的,取到就直接返回所有加载项,如果里面没有值,才会继续往下执行。
3、引入ClassLoader【自动加载的核心】
我们假装第一次来,所以我们继续往下看:
spl_autoload_register(array('ComposerAutoloaderInit1a384f2130f559ec4821f173d482452a', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit1a384f2130f559ec4821f173d482452a', 'loadClassLoader'));
上面这一部分要和下面这个函数一起看
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
这两段代码合起来的效果就是先引入这个文件,实例化之后赋值给静态属性$loader
和变量$loader
,然后把这个引入销毁掉。
我们继续往下看代码:
$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\ComposerStaticInit1a384f2130f559ec4821f173d482452a::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);
}
}
首先判断PHP版本,如果是小于5.6,就用静态调用的方式,因为PHP5.6以下不支持命名空间,后面的HHVM_VERSION
是虚拟机扩展和zend_loader_file_encoded
PHP文件加密,一般用不到,所以这边就不详细展开讲了。
4、用匿名函数给对象的私有属性赋值
我们先看PHP版本>5.6的情况:
require_once __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit1a384f2130f559ec4821f173d482452a::getInitializer($loader));
我们先来看这个方法getInitializer($loader)
做了什么:
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit1a384f2130f559ec4821f173d482452a::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit1a384f2130f559ec4821f173d482452a::$prefixDirsPsr4;
$loader->fallbackDirsPsr0 = ComposerStaticInit1a384f2130f559ec4821f173d482452a::$fallbackDirsPsr0;
$loader->classMap = ComposerStaticInit1a384f2130f559ec4821f173d482452a::$classMap;
}, null, ClassLoader::class);
我们可以看到,这里是用Closure::bind
来复制了一个闭包,绑定到ClassLoader
的类作用域,为什么用匿名函数来绑定呢,因为ClassLoader $loader
中的这四个属性是private
的,不能在外边直接赋值,所以用匿名函数绑定的方式来给这几个函数赋值,最终会返回一个可以对prefixLengthsPsr4
、prefixDirsPsr4
、fallbackDirsPsr0
、classMap
这四个属性赋值的匿名函数。
接下来就是用call_user_func()
来执行这个匿名函数,就是上面四个属性被赋值。
5、SPL自动注册
下面就到了最有趣的地方,也是自动加载的真相:
$loader->register(true);
从这行代码我们去往下找:
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
可以看到有个spl_autoload_register()
,这个操作注册了一个方法,就是当前类的loadClass
方法,spl_autoload_register()
函数就是注册给定的函数作为__autoload
的实现,也就是说是把loadClass
做为了自动加载器,每当有不明的类、命名空间就会自动调用loadClass
方法:
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
上面的方法又调用了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;
}
在上面这个方法中,如果在classMap
中找到了具体的类就会直接返回,否则继续往下执行findFileWithExtension()
方法,findFileWithExtension()
方法的代码太多就不往上贴了,可以参考源码,这个方法的作用就是在上面被赋值的prefixLengthsPsr4
、prefixDirsPsr4
、fallbackDirsPsr0
三个属性里循环查找,最终找到类或命名空间所在位置,然后include
进来,使得程序可以正常执行。
6、加载全局函数库
最后一段代码就比较平常了:
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInit1a384f2130f559ec4821f173d482452a::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire1a384f2130f559ec4821f173d482452a($fileIdentifier, $file);
}
这一段是循环引入了$files
中的所有的全局函数库。
public static $files = array (
'9b552a3cc426e3287cc811caefa3cf53' => __DIR__ . '/..' . '/topthink/think-helper/src/helper.php',
'538ca81a9a966a6716601ecf48f4eaef' => __DIR__ . '/..' . '/opis/closure/functions.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
);
function composerRequire1a384f2130f559ec4821f173d482452a($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}
然后最后把$loader
给return
回去:
return $loader;
随着不断的return
,最终回到了入口文件index.php
。
自动加载完成!