Composer自动加载原理

Composer

Composer是 用PHP开发的用来管理项目依赖的工具,当你在项目中声明了依赖关系后,composer可以自动帮你下载和安装这些依赖库,并实现自动加载代码。

定义一个composer.json:

1
2
3
4
5
6
{
"name": "gitlib/composer",
"require":{
"predis/predis":"1.1.1"
}
}

输入命令 composer install,composer会帮我们自动下载predis库,依赖库会默认放在项目的vendor目录下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
├── composer.json
├── composer.lock
├── index.php
└── vendor
├── autoload.php
├── composer
│   ├── ClassLoader.php
│   ├── LICENSE
│   ├── autoload_classmap.php
│   ├── autoload_namespaces.php
│   ├── autoload_psr4.php
│   ├── autoload_real.php
│   ├── autoload_static.php
│   └── installed.json
└── predis
└── predis

composer不仅仅帮我们处理依赖,还帮我们实现了自动加载。在vendor目录下有一个autoload.php, 只要在我们的项目中引入这个文件就可以自动加载依赖库。

1
2
3
4
5
6
7
8
<?php

require 'vendor/autoload.php';

$client = new Predis\Client();
$client->set('foo', 'bar');
$value = $client->get('foo');
echo $value;

可以看到Predis库完全不需要我们手动去加载,只需要require 'vendor/autoload.php',composer的自动加载机制会帮我们找到对应的文件并加载。

对于依赖库,composer帮我们处理好了自动加载, 那对于其他的类库,如何实现自动加载呢?

composer支持四种自动加载的方式:Files/Classmap/PSR-0/ PSR-4, 其中PSR-4是当前推荐的加载方式。

Files

Files 是最简单的加载方式,这种方式不管加载的文件是否用到始终都会加载,而不是按需加载, 修改项目根目下的composer.json, 加入 “autoload” 项:

1
2
3
4
5
6
7
8
9
{
"name": "gitlib/composer",
"require":{
"predis/predis":"1.1.1"
},
"autoload":{
"files":["Controller/User.php"]
}
}

files键对应的值是一个数组,数组元素是文件的路径,路径是相对于应用的根目录。加上上述内容后,运行命令:

composer dump-autoload

让composer重建自动加载的信息,composer会把配置值写入与 Files加载方式对应的 verndor\composer\autoload_files.php配置文件中:

1
2
3
4
5
6
7
8
9
10
<?php

// autoload_files.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
'7efd69bb86214589340b40039fd363f7' => $baseDir . '/Controller/User.php',
);

现在就可以在代码中里调用User类了。

1
2
3
4
5
6
7
8
<?php

require 'vendor/autoload.php';

$client = new Predis\Client();

$user = new \Controller\User();
$user->login();

Classmap

classmap引用的所有组合,都会在 install/update 过程中生成,并存储到vendor/composer/autoload_classmap.php 文件中。这个 map 是经过扫描指定目录(同样支持直接精确到文件)中所有的 .php.inc 文件里内置的类而得到的。

1
2
3
4
5
6
7
8
9
{
"name": "gitlib/composer",
"require":{
"predis/predis":"1.1.1"
},
"autoload":{
"classmap":["Controller"]
}
}

Composer会扫描Controller目录下的所有.php.inc文件,存储到vendor/composer/autoload_classmap.php文件中:

1
2
3
4
5
6
7
8
9
10
<?php

// autoload_classmap.php @generated by Composer

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);

return array(
'Controller\\User' => $baseDir . '/Controller/User.php',
);

PSR-0

PSR-0自动加载规范是已经废弃的标准, 不再做说明。

PSR-4

PSR-4是Composer推荐使用的一种方式(关于PSR规范可参考:PHP标准规范PSR),因为它更易使用并能带来更简洁的目录结构。对于上面的Controller目录我们先改名src:

1
2
3
4
5
6
7
8
9
10
├── composer.json
├── composer.json.bk
├── composer.lock
├── index.php
├── src
│   └── User.php
└── vendor
├── autoload.php
├── composer
└── predis

在composer.json中我们将Controller命名空间和src关联起来:

1
2
3
4
5
6
7
8
9
10
11
{
"name": "gitlib/composer",
"require":{
"predis/predis":"1.1.1"
},
"autoload":{
"psr-4": {
"Controller\\":"src/"
}
}
}

PSR-4 的命名空间前缀也必须以 \\ 结尾,以避免类似前缀间的冲突。

psr-4中的key和value定义了namespace以及其对应的目录映射。按照PSR-4的规则,当试图自动加载”Controller\User”类的使用,会去寻找”src/User.php”这个文件,此时Controller并不会出现在文件路径中。

自动加载原理

下面我们通过源码分析composer是如何实现自动加载功能。

入口

1
2
3
<?php

require 'vendor/autoload.php';

我们通过require ‘vendor/autoload.php实现自动加载,vendor/autoloaad.php文件引用composer/autoload_real.php

1
2
3
4
5
6
7
<?php

// autoload.php @generated by Composer

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

return ComposerAutoloaderInitb84761f57e62a6a534584b91ca213591::getLoader();

autoload_real

autoload_real.php是自动加载引导类,程序主要调用了引导类的静态方法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
69
70
71
<?php

// autoload_real.php @generated by Composer

class ComposerAutoloaderInitb84761f57e62a6a534584b91ca213591
{
private static $loader;

public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}

public static function getLoader()
{
// 返回Composer\Autoload\ClassLoader单例
if (null !== self::$loader) {
return self::$loader;
}

// 调用spl_autoload_register加载\Composer\Autoload\ClassLoader
spl_autoload_register(array('ComposerAutoloaderInitb84761f57e62a6a534584b91ca213591', 'loadClassLoader'), true, true);
// 实例化\Composer\Autoload\ClassLoader类
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInitb84761f57e62a6a534584b91ca213591', 'loadClassLoader'));

// 静态初始化只支持 PHP5.6 以上版本并且不支持 HHVM 虚拟机
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
// 使用 autoload_static 进行静态初始化
require_once __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInitb84761f57e62a6a534584b91ca213591::getInitializer($loader));
} else {
// 如果PHP版本低于 5.6 或者使用 HHVM 虚拟机环境,那么就要使用核心类的接口进行初始化
// PSR0 标准
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}

// PSR4 标准
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}

// classmap
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
}

$loader->register(true);

// files
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInitb84761f57e62a6a534584b91ca213591::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
// files定义的文件,直接require就行了
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequireb84761f57e62a6a534584b91ca213591($fileIdentifier, $file);
}

return $loader;
}
}

autoload_static

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
<?php

// autoload_static.php @generated by Composer

namespace Composer\Autoload;

class ComposerStaticInitb84761f57e62a6a534584b91ca213591
{
public static $files = array (
'7efd69bb86214589340b40039fd363f7' => __DIR__ . '/../..' . '/Controller/User.php',
);

public static $prefixLengthsPsr4 = array (
'P' =>
array (
'Predis\\' => 7,
),
'C' =>
array (
'Controller\\' => 11,
),
);

public static $prefixDirsPsr4 = array (
'Predis\\' =>
array (
0 => __DIR__ . '/..' . '/predis/predis/src',
),
'Controller\\' =>
array (
0 => __DIR__ . '/../..' . '/src',
),
);

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

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

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

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

因为 PSR4 标准是用顶级命名空间目录替换顶级命名空间,所以获得顶级命名空间的长度很重要。

ClassLoader

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
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}

public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);

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

function includeFile($file)
{
include $file;
}

ClassLoader 的 register() 函数将 loadClass() 函数注册到 PHP 的 SPL 函数堆栈中,每当 PHP 遇到不认识的命名空间时就会调用函数堆栈的每个函数,直到加载命名空间成功。所以 loadClass() 函数就是自动加载的关键了。

有用就打赏一下作者吧!