Laravel基于Composer实现自动加载原理分析

一般的框架中都会使用 Composer 工具进行包管理,Composer 有自动加载机制可以自动加载框架中所依赖 的类库等文件,那么它的自动加载机制是怎么实现的呢?本篇文章将介绍 Laravel 框架中 Composer 的自动加载原理。

目录

  • 从入口文件开始
  • getLoader 方法
  • loadClassLoader 方法
  • getInitializer 方法
  • Composer 包管理类自动加载注册
  • findFile 方法
  • findFileWithExtension 方法
  • 自动加载文件数组
  • 总结

从入口文件开始

首先,Laravel 框架在路口文件 public/index.php 中引入了类加载器:

1
require __DIR__.'/../vendor/autoload.php';

autoload.php是由 Composer 在初始化的时候生成,进入vendor/autoload.php文件中可以看到:

1
2
3
4
5
6
// autoload.php @generated by Composer

require_once __DIR__ . '/composer/autoload_real.php';

// ComposerAutoloaderInit 后面的 hash 字符串代表不同项目的唯一标识符
return ComposerAutoloaderInit101671ca9bbc2f62f8335eb842637291::getLoader();

加载 vendor/composer/autoload_real.php 文件,并且返回 vendor/composer/autoload_real.php 文件中ComposerAutoloaderInitXXX 类的 getLoader 函数。

getLoader 方法

接下来进入 本篇文章的重点 getLoader 函数,源码和注解如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
//  位于 ComposerAutoloaderInitXXX 类中
public static function getLoader()
{
// 设置 私有的静态属性 $loader,用于提高性能,如果类加载器已经初始化过,则直接返回,否则继续执行
if (null !== self::$loader) {
return self::$loader;
}

require __DIR__ . '/platform_check.php';
// 通过 PHP 内置函数 spl_autoload_register 注册类加载器
spl_autoload_register(array('ComposerAutoloaderInit101671ca9bbc2f62f8335eb842637291', 'loadClassLoader'), true, true);
// 对静态属性$loader进行赋值
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
// 通过 spl_autoload_unregister 函数注销之前注册的类加载器。
spl_autoload_unregister(array('ComposerAutoloaderInit101671ca9bbc2f62f8335eb842637291', 'loadClassLoader'));

// 从 5.6 版本开始 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';

// 设置 $loader 实例的 prefixLengthsPsr4、prefixDirsPsr4、prefixesPsr0、classMap 属性
call_user_func(\Composer\Autoload\ComposerStaticInit101671ca9bbc2f62f8335eb842637291::getInitializer($loader));
} else {
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
// 设置 prefixesPsr0 属性
$loader->set($namespace, $path);
}

$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
// 设置 prefixLengthsPsr4 和 prefixDirsPsr4 属性
$loader->setPsr4($namespace, $path);
}

$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
// 设置 classMap 属性
$loader->addClassMap($classMap);
}
}
// 其中 classMap 管理的是完整类名(可能包含命名空间)与文件路径的映射关系
// prefixDirsPsr4 管理的是命名空间与文件目录的映射关系(遵循 psr-4 规范)
// prefixLengthsPsr4 管理的是命名空间前缀长度
// prefixesPsr0 管理也是命名空间与文件目录映射关系(遵循 psr-0 规范,极少数历史包在使用,目前 psr-0 规范已废弃)

// 完成 Composer 包管理器类自动加载注册
$loader->register(true);

// 在 Laravel 项目中,除了类之外,还支持不归属于任何类的辅助函数,
//这些辅助函数通常定义在 helpers.php 文件中,Composer 通用支持对这类文件的自动加载,这一块的处理通样针对是否支持静态初始化进行了区分

// 判断是否支持静态初始化
if ($useStaticLoader) {
// 如果支持静态初始化,则通过 Composer\Autoload\ComposerStaticInitXXX::$files; 返回自动加载的文件数组
$includeFiles = Composer\Autoload\ComposerStaticInit101671ca9bbc2f62f8335eb842637291::$files;
} else {
// 否则的话引入 vendor/composer/autoload_files.php 返回需要自动加载的文件数组
$includeFiles = require __DIR__ . '/autoload_files.php';
}
// 最后遍历这些文件逐个引入,并将它们的标识符存放到全局变量 __composer_autoload_files 中。
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire101671ca9bbc2f62f8335eb842637291($fileIdentifier, $file);
}

return $loader;
}

ComposerAutoloaderInitXXX 类的定义了私有静态属性 $loader用于判断一次HTTP请求过程中类是否被实例化过,用于提高性能,如果类加载器已经初始化过,则直接返回,否则继续执行。

接着通过 PHP 内置函数 spl_autoload_register 注册类加载器 loadClassLoader

将 ClassLoader类加载进来后,实例化ClassLoader类将它赋值给静态属性 $loader

loadClassLoader 方法

用于自动加载 ClassLoader 类

1
2
3
4
5
6
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}

getInitializer 方法

进入Composer类加载机制的核心,PHP5.6 后支持静态初始化,对于满足静态初始化条件的,则进入getInitializer函数体 内,设置 $loader 实例的 prefixLengthsPsr4、prefixDirsPsr4、prefixesPsr0、classMap 属性

1
2
3
4
5
6
7
8
9
10
11
// composer/autoload_static.php
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit101671ca9bbc2f62f8335eb842637291::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit101671ca9bbc2f62f8335eb842637291::$prefixDirsPsr4;
$loader->prefixesPsr0 = ComposerStaticInit101671ca9bbc2f62f8335eb842637291::$prefixesPsr0;
$loader->classMap = ComposerStaticInit101671ca9bbc2f62f8335eb842637291::$classMap;

}, null, ClassLoader::class);
}

Composer 包管理类自动加载注册

1
2
// 完成 Composer 包管理器类自动加载注册
$loader->register(true);

register 函数运行代码如下:

1
2
3
4
5
// composer/ClassLoader.php
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}

调用 $loader 的 loadClass 方法,加载给定的类或接口:

1
2
3
4
5
6
7
8
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);

return true;
}
}

findFile 方法

查找对应类所在文件路径并引入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public function findFile($class)
{
// class map lookup
// 判断 $loader->classMap 中是否包含该类
if (isset($this->classMap[$class])) {
// 包含则直接 返回对应文件路径
return $this->classMap[$class];
}
// 如果该类标识为缺失,则不再往下执行
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
// 如果返回值为 false,并且定义了 HHVM_VERSION 常量,则将文件后缀改为 .hh 再找一次
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}

// 如果启用了 apcu 扩展,不管找没找到,都将结果保存到 apcu 缓存。
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}

// 如果最后文件路径没有找到,则将其添加到 $loader->missingClasses 属性中
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}

findFileWithExtension 方法

先通过 PSR-4 规范查找类对应文件路径,如果找到则返回。

否则再通过 PSR-0 规范查找

这其中会用到 $loader 初始化时设置的 prefixLengthsPsr4、prefixDirsPsr4、prefixesPsr0 属性来组合出查找条件。

如果最终没有找到,则返回 false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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;
}
}

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

接下来我们返回到 ComposerAutoloaderInitXXX 类中中的 getLoader 函数,继续执行后续代码。

自动加载文件数组

在 Laravel 项目中,除了类之外,还支持不归属于任何类的辅助函数,通常定义在 app/helpers.php 文件中,Composer对于这类文件的自动加载和类加载一样同样根据是否支持静态初始化,进行了区分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 判断是否支持静态初始化
if ($useStaticLoader) {
// 如果支持静态初始化,则通过 Composer\Autoload\ComposerStaticInitXXX::$files; 返回自动加载的文件数组
$includeFiles = Composer\Autoload\ComposerStaticInit101671ca9bbc2f62f8335eb842637291::$files;
} else {
// 否则的话引入 vendor/composer/autoload_files.php 返回需要自动加载的文件数组
$includeFiles = require __DIR__ . '/autoload_files.php';
}
// 最后遍历这些文件逐个引入,并将它们的标识符存放到全局变量 __composer_autoload_files 中。
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire101671ca9bbc2f62f8335eb842637291($fileIdentifier, $file);
}

return $loader;

最后,项目根目录下用户自定义的 PHP 文件,需要在 composer.json 的 autoload.files中配置:

1
2
3
4
5
"autoload": {
...
"files": [
"app/helpers.php"
]

总结

Composer 支持 4 种自动加载方式:classmap、psr-4、psr-0 和 files 四个配置,前三个都是维护类的自动加载,最后一个维护的是文件的自动加载。

classmap:常用来管理不归属于命名空间的类,比如 databases 目录下的类,在配置的时候只需要配置目录路径即可。

psr-4:通常用来管理归属于命名空间的类,我们在配置的时候只需配置根命名空间与对应目录的映射即可

psr-0 :已废弃,很少使用

files:需要配置完整的文件路径

PS:喜欢的朋友可以关注公众号 苏小怪的梦呓

Laravel基于Composer实现自动加载原理分析

http://fahsa.cn/php/laravel-composer/

作者

Fahsa

发布于

2021-03-02

更新于

2021-03-04

许可协议

评论