自动加载(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_encodedPHP文件加密,一般用不到,所以这边就不详细展开讲了。

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的,不能在外边直接赋值,所以用匿名函数绑定的方式来给这几个函数赋值,最终会返回一个可以对prefixLengthsPsr4prefixDirsPsr4fallbackDirsPsr0classMap这四个属性赋值的匿名函数。

接下来就是用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()方法的代码太多就不往上贴了,可以参考源码,这个方法的作用就是在上面被赋值的prefixLengthsPsr4prefixDirsPsr4fallbackDirsPsr0三个属性里循环查找,最终找到类或命名空间所在位置,然后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;
    }
}

然后最后把$loaderreturn回去:

return $loader;

随着不断的return,最终回到了入口文件index.php

自动加载完成!