/

Laravel 中 composer 加载流程

启动

  • Laravel 5.8

文章以 Laravel 学习。入口文件 public/index.php

// Register The Auto Loader
require __DIR__.'/../vendor/autoload.php';

autoload.php 不负责具体功能逻辑,只做了两件事:初始化自动加载类、注册自动加载类。

autoload_real.php 中的类名为 ComposerAutoloaderInit... 这可能是为防止与用户自定义类名跟这个类重复冲突,加上了哈希值。

其实还有一个做法我们更加熟悉,是定义一个命名空间。这里为什么不定义一个命名空间呢?一种理解:命名空间一般都是为了复用,而这个类只需要运行一次即可,以后也不会用得到,用哈希值更加合适。

autoload_real.php

autoload.php 主要调用了 getLoader()

public static function getLoader()
{
// 单例模式,自动加载类只能有一个 1
if (null !== self::$loader) {
return self::$loader;
}

// 获得自动加载核心类对象 2
spl_autoload_register(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'));

// 初始化自动加载核心类对象 3
$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\ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::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);
}
}

// 注册自动加载核心类对象 4
$loader->register(true);

// 自动加载全局函数 5
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire76e88f0b305cd64c7c84b90b278c31db($fileIdentifier, $file);
}

return $loader;
}

单例模式 1

if (null !== self::$loader) {
return self::$loader;
}

构造 ClassLoader 核心类 2

spl_autoload_register(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'));
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}

composer 先向 PHP 自动加载机制注册了一个函数,这个函数 requireClassLoader 文件。成功 new 出该文件中核心类 ClassLoader() 后,又销毁了该函数。

为什么不直接 require?原因是:怕有的用户也定义了个 \Composer\Autoload\ClassLoader 命名空间,导致自动加载错误文件。

那为什么不跟引导类一样用个哈希值呢?原因是:这个类是可以复用的,框架允许用户使用这个类。

初始化核心类对象 3

对自动加载类的初始化,主要是给自动加载核心类初始化顶级命名空间映射。初始化的方法有两种:

  1. 使用 autoload_static 进行静态初始化
  2. 调用核心类接口初始化

autoload_static 静态初始化

静态初始化只支持 PHP 5.6 以上版本、不支持 HHVM 虚拟机、不存在 Zend-encoded file

autoload_static.php

<?php

// autoload_static.php @generated by Composer

namespace Composer\Autoload;

// hash 防止冲突
class ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db
{
public static $files = array (...);
public static $prefixLengthsPsr4 = array (...);
public static $prefixDirsPsr4 = array (...);
public static $fallbackDirsPsr4 = array (...);
public static $prefixesPsr0 = array (...);
public static $classMap = array array (...);

public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$prefixDirsPsr4;
$loader->fallbackDirsPsr4 = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$fallbackDirsPsr4;
$loader->prefixesPsr0 = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$prefixesPsr0;
$loader->classMap = ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$classMap;

}, null, ClassLoader::class);
}
}

这个静态初始化类的核心就是 getInitializer() 函数,它将自己类中的顶级命名空间映射给了 ClassLoader 类。

值得注意的是这个函数返回的是一个匿名函数,为什么呢?原因就是 ClassLoader 中的 prefixLengthsPsr4prefixDirsPsr4 等等方法都是 private 的。普通的函数没办法给类的 private 成员变量赋值。利用匿名函数的绑定功能就可以将把匿名函数转为 ClassLoader 类的成员函数。

关于匿名函数的 绑定功能

接下来就是 顶级命名空间 初始化的关键了。

classMap

public static $classMap = array (
'App\\Api\\Middleware\\DeviceRecord' => __DIR__ . '/../..' . '/app/Api/Middleware/DeviceRecord.php',
'App\\Api\\Middleware\\HeaderCheck' => __DIR__ . '/../..' . '/app/Api/Middleware/HeaderCheck.php',
...
)

直接命名空间全名与目录的映射,没有顶级命名空间。简单粗暴,也导致这个数组相当的大。

PSR0 顶级命名空间映射

public static $prefixesPsr0 = array (
'P' =>
array (
'Prophecy\\' =>
array (
0 => __DIR__ . '/..' . '/phpspec/prophecy/src',
),
'Parsedown' =>
array (
0 => __DIR__ . '/..' . '/erusev/parsedown',
),
),
...
);

为了快速找到顶级命名空间,这里使用命名空间第一个字母作为前缀索引。这个映射的用法比较明显,假如我们有 Parsedown/example 这样的命名空间,首先通过首字母 P,找到:

'P' => array (...)

这个数组,然后就会遍历这个数组来和 Parsedown/example 比较,发现第一个 Prophecy 不符合,第二个 Parsedown 符合,然后得到了映射目录(映射目录可能不止一个):

0 => __DIR__ . '/..' . '/erusev/parsedown',

接着遍历这个数组,尝试 __DIR__ . '/..' . '/erusev/parsedown/Parsedown/example.php' 是否存在,如果不存在接着遍历数组(这个例子数组只有一个元素),如果数组遍历完都没有,就会加载失败。

PSR4 标准顶级命名空间映射

public static $prefixLengthsPsr4 = array (
'p' =>
array (
'phpDocumentor\\Reflection\\' => 25,
),
'Z' =>
array (
'Zend\\Diactoros\\' => 15,
),
...
);

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',
),
'Zend\\Diactoros\\' =>
array (
0 => __DIR__ . '/..' . '/zendframework/zend-diactoros/src',
),
...
);

PSR4 标准 顶级命名空间 映射用了两个数组,第一个和 PSR0 一样用命名空间第一个字母作为前缀索引,然后是 顶级命名空间,但是最终并不是文件路径,而是 顶级命名空间 的长度。为什么呢?因为 PSR4 的文件目录更加灵活,更加简洁。

PSR0顶级命名空间 目录 直接加 到命名空间前面就可以得到路径:

                                        ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
Parsedown/example => __DIR__ . '/..' . '/erusev/parsedown/Parsedown/example.php
↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

PSR4 却是用顶级命名空间目录 替换 顶级命名空间,所以获得顶级命名空间的 长度 很重要:

                                        ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
Parsedown/example => __DIR__ . '/..' . '/erusev/parsedown/example.php
↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

举例:假如我们找 Symfony\\Polyfill\\Mbstring\\example 这个类,和 PSR0 一样通过前缀索引和字符串匹配我们得到了:

'Symfony\\Polyfill\\Mbstring\\' => 26,

这条记录,键是顶级命名空间,值是命名空间的长度。拿到顶级命名空间后去 $prefixDirsPsr4 获取它的映射目录数组(注意映射目录可能不止一条):

'Symfony\\Polyfill\\Mbstring\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
),

Symfony\\Polyfill\\Mbstring\\example 前 26 个字母替换为 __DIR__ . '/..' . '/symfony/polyfill-mbstring 也就是:

__DIR__ . '/..' . '/symfony/polyfill-mbstring/example.php

先验证磁盘上这个文件是否存在,如果不存在接着遍历。如果遍历后没有找到,则加载失败。

自动加载核心类 ClassLoader 的静态初始化完成!

其实还有 $fallbackDirsPsr4,暂未研究

调用接口初始化

如果 PHP 版本低于 5.6 或者使用 HHVM 虚拟机环境或者存在 zend_loader_file_encoded,那么就要使用核心类的接口进行初始化。


/*
PSR0 取出命名空间的第一个字母作为索引,一个索引对应多个顶级命名空间,一个顶级命名空间对应多个目录路径,具体形式可以查看上面的 autoload_static 的 $prefixesPsr0。

如果没有顶级命名空间,就只存储一个路径名,以便在后面尝试加载。
*/
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}

/*
PSR4 如果没有顶级命名空间,就直接保存目录。
如果有命名空间的话,要保证顶级命名空间最后是 \,然后分别保存
( 前缀 -> 顶级命名空间,顶级命名空间 -> 顶级命名空间长度 )
( 顶级命名空间 -> 目录 )

这两个映射数组。具体形式可以查看上面我们讲的 autoload_static 的 prefixLengthsPsr4、$prefixDirsPsr4 。
*/
$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);
}

注册核心类对象 4

Composer 自动加载功能的启动与初始化,经过启动与初始化,自动加载核心类对象已经获得了顶级命名空间与相应目录的映射,换句话说,如果有命名空间 App\Console\Kernel,我们已经知道了 App\ 对应的目录,接下来我们就要解决下面的就是 \Console\Kernel 这一段。

/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}

一行代码实现自动加载。核心在 ClassLoaderloadClass() 函数上,这个函数负责按照 PSR 标准将顶层命名空间以下的内容转为对应的目录,也就是上面所说的将 App\Console\KernelConsole\Kernel 这一段转为目录。

自动加载全局函数 5

Composer 不止可以自动加载命名空间,还可以加载全局函数。就是把全局函数写到特定的文件里面去,在程序运行前挨个 require 就行了。

if ($useStaticLoader) {
// 静态初始化
$includeFiles = Composer\Autoload\ComposerStaticInit76e88f0b305cd64c7c84b90b278c31db::$files;
} else {
// 普通初始化
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire76e88f0b305cd64c7c84b90b278c31db($fileIdentifier, $file);
}
function composerRequire76e88f0b305cd64c7c84b90b278c31db($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;

$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}

问题 1

为什么不直接 require $includeFiles 里面的每个文件名,而要用类外面的函数 composerRequire...

  • 避免和用户定义函数冲突
  • 防止有人在全局函数所在的文件写 $this 或者 self

假如 $includeFiles 有个 app/helper.php 文件,这个 helper.php 文件的函数外有一行代码: $this->foo(),如果引导类在 getLoader() 函数直接 require($file),那么引导类就会运行这句代码,调用自己的 foo() 函数,这显然是错的。

事实上 helper.php 就不应该出现 $thisself 这样的代码,这样写一般都是用户写错了的,一旦这样的事情发生:

  • 第一种情况:引导类恰好有 foo() 函数,那么就会莫名其妙执行了引导类的 foo()
  • 第二种情况:引导类没有 foo() 函数,但是却甩出来引导类没有 foo() 方法这样的错误提示,用户不知道自己哪里错了。把 require 语句放到 引导类的外面,遇到 $this 或者 self ,程序就会告诉用户根本没有类, $thisself 无效,错误信息更加明朗。

问题 2

为什么要用 hash 作为 $fileIdentifier

这个变量是用来控制全局函数只被 require 一次的,那为什么不用 require_once 呢?事实上 require_oncerequire 效率低很多,使用全局变量 $GLOBALS 这样控制加载会更快。猜测另一个原因应该是 require_once 对相对路径的支持并不理想,所以 composer 尽量少用 require_once

运行

ClassLoaderloadClass() 函数注册到 PHP SPL 中的 spl_autoload_register() 里面去。这样,每当 PHP 遇到一个不认识的命名空间的时候,PHP 会自动调用注册到 spl_autoload_register() 里面的函数堆栈,运行其中的每个函数,直到找到命名空间对应的文件。

/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return bool|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file); // include $file; Prevents access to $this/self from included files.

return true;
}
}

/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}

// classMapAuthoritative 关闭搜索未在类映射中注册的类的 prefix and fallback directories。- 不清楚干啥的 暂没研究
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}

// 如果启用扩展名,则使用 APCu 前缀来缓存已找到/未找到的类。 - 不清楚干啥的 暂没研究
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() 包含了 PSR0PSR4

如果我们在代码中写 'phpDocumentor\Reflection\example,PHP 会通过 SPL 调用 loadClass -> findFile -> findFileWithExtension

  • 首先默认用 .php 后缀名调用 findFileWithExtension 函数里,利用 PSR4 标准尝试解析目录文件,如果文件不存在则继续用 PSR0 标准解析
  • 如果解析出来的目录文件仍然不存在,但是环境是 HHVM 虚拟机,继续用后缀名 .hh 再次调用 findFileWithExtension 函数,如果不存在,说明此命名空间无法加载,放到 classMap 中设为 false,以便以后更快地加载

PSR4

对于 phpDocumentor\Reflection\example,当尝试利用 PSR4 标准映射目录时,步骤如下:

// $class: phpDocumentor\Reflection\example

// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
// $logicalPathPsr4: phpDocumentor/Reflection/example.php(hh)`

$first = $class[0];
// $first: p

if (isset($this->prefixLengthsPsr4[$first])) {
/* 'p' =>
array (
'phpDocumentor\\Reflection\\' => 25,
),
*/
$subPath = $class;
// $subPath: phpDocumentor\Reflection\example
while (false !== $lastPos = strrpos($subPath, '\\')) {
// $lastPos 13
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath.'\\';
if (isset($this->prefixDirsPsr4[$search])) {
// search phpDocumentor\\Reflection\\
// $lastPos 25

/* 'phpDocumentor\\Reflection\\' =>
array (
0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',
1 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src',
2 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',
),
*/
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
// $pathEnd /example.php(hh)

foreach ($this->prefixDirsPsr4[$search] as $dir) {
// 遍历 3 个
if (file_exists($file = $dir . $pathEnd)) {
// $file __DIR__ . '/..' . /phpdocumentor/type-resolver/src/example.php(hh)`
return $file;
}
}
}
}
}

PSR0

如果 PSR4 标准加载失败,则要进行 PSR0 标准加载。对于 phpDocumentor\Reflection\example,当尝试利用 PSR0 标准映射目录时,步骤如下:

// $class: phpDocumentor\Reflection\example

// 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;
}

// $logicalPathPsr0: phpDocumentor/Reflection/example.php(hh)`
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
/* 'P' =>
array (
'Prophecy\\' =>
array (
0 => __DIR__ . '/..' . '/phpspec/prophecy/src',
),
'Parsedown' =>
array (
0 => __DIR__ . '/..' . '/erusev/parsedown',
),
), */
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
// $file __DIR__ . '/..' . '/phpspec/prophecy/src' . phpDocumentor/Reflection/example.php(hh)
return $file;
}
}
}
}
}

Q&A

个人一些疑问:

防止用户自定义与 ClassLoader 命名空间冲突

spl_autoload_register(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit76e88f0b305cd64c7c84b90b278c31db', 'loadClassLoader'));

为什么这样可以解决:与用户也定义了个 \Composer\Autoload\ClassLoader 命名空间,导致自动加载错误文件。

与第四个参数 $prepend true 有关吗?

composer StaticLoader 有什么优势

composer 在加载类和加载全局方法时,都有两种方式。

$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());

$useStaticLoader 的值进行选择,为什么一定分两种,静态方法是有什么优势吗?

References

– EOF –