PHP Composer 的自动加载
Tags: php, composer
Published: 2023-09-07 15:42:56


PHP 的 autoload 机制，可以在使用一个未导入的类时动态加载该类，从而实现延迟加载和管理依赖类文件的目的。

### 一、没有 composer 时 PHP 是怎么做的

**__autoload 自动加载器**

PHP 中想要使用一个类，必须通过 `require` (指代 require_once, include_once 等) 的方式在文件开头声明要使用的类。当项目中类较多时，一个个声明加载显然不可行。

在 PHP5 版本，PHP 支持通过 `__autoload` 定义一个自动加载器，尝试加载未定义的类。 如：

```php
// we've writen this code where we need
function __autoload($classname) {
    $filename = "./". $classname .".php";
    include_once($filename);
}

// we've called a class ***
$obj = new myClass();
```

但 `__autoload` 函数缺点比较明显：他只能定义一次，这样就会耦合所有依赖的类的自动加载逻辑，统统写到这个方法里，这时候就需要用到 `spl_autoload_register` 函数了。

使用 **spl_autoload_register 注册多个自动加载器**

spl 是 standard php library 的缩写。`spl_autoload_register` 最大的特点是支持注册多个自动加载器，这样就能实现将各个类库的自动加载逻辑分开，自己处理自己的加载逻辑。

```php
function my_autoloader($class) {
    var_dump("my_autoloader", $class);
}

spl_autoload_register('my_autoloader');

// 静态方法
class MyClass1 {
    public static function autoload($className) {
        var_dump("MyClass1 autoload", $className);
    }
}

spl_autoload_register(array('MyClass1', 'autoload'));

// 非静态方法
class MyClass2 {
    public function autoload($className) {
        var_dump("MyClass2 autoload", $className);
    }
}

$instance = new MyClass2();
spl_autoload_register(array($instance, 'autoload'));

new \NotDefineClassName();

/*
输出
string(32) "my_autoloader NotDefineClassName"
string(36) "MyClass1 autoload NotDefineClassName"
string(36) "MyClass2 autoload NotDefineClassName"
*/
```

### 二、PSR 规范

PSR 即 PHP Standards Recommendation 是一个社区组织：https://www.php-fig.org/psr/，声明一系列规范来统一开发风格，减少互不兼容的困扰。规范中的 PSR-4 代表：Autoloading Standard，即自动加载规范。

**PSR-4**

其中规定：一个类的完整类名应该遵循一下规范：

`\<命名空间>(\<子命名空间>)*\<类名>`，即：

1. 完整的类名**必须**要有一个顶级命名空间，被称为 “vendor namespace”；
2. 完整的类名**可以**有一个或多个子命名空间；
3. 完整的类名**必须**有一个最终的类名；
4. 完整的类名中任意一部分中的下滑线都是没有特殊含义的；
5. 完整的类名**可以**由任意大小写字母组成；
6. 所有类名都**必须**是大小写敏感的。

看看例子：

![PSR-4](https://deb-gmi.oss-cn-beijing.aliyuncs.com/img/20210324180507.png)

应用的效果简单来说就是：将命名空间前缀 Namespace Prefix 替换成 Base Directory 目录，并将 \ 替换成 / 。**一句话，命名空间可以表明类具体的存放位置。**

### 三、Composer 自动加载的过程

结合 `spl_auto_register` 和 PSR-4 的命名空间规范，可以想象，我们可以通过类的命名空间，来找到具体类的存放位置，然后通过 require 将其加载进来生效，composer 就是这么干的。

接下来我们分两步看 composer 是怎么做的。

**第一步，建立类的命名空间和类存放位置的映射关系**

首先看 vendor 目录下的 autoload.php 文件，所有项目启动必然要先 require 这个文件。

```php
// autoload.php @generated by Composer
// vendor/autoload.php
require_once __DIR__ . '/composer/autoload_real.php';

// 返回了autoload_real文件中的类方法
return ComposerAutoloaderInit7e421c277f7e8f810a19524f0d771cdb::getLoader();

/* ------------- */

// vendor/composer/autoload_real.php
public static function getLoader()
{
    if (null !== self::$loader) {
        return self::$loader;
    }

    // P0 初始化ClassLoader
    spl_autoload_register(array('ComposerAutoloaderInit7e421c277f7e8f810a19524f0d771cdb', 'loadClassLoader'), true, true);
    self::$loader = $loader = new \Composer\Autoload\ClassLoader();
    spl_autoload_unregister(array('ComposerAutoloaderInit7e421c277f7e8f810a19524f0d771cdb', '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';

        // P1 向ClassLoader中set命名空间和文件路径映射关系
        call_user_func(\Composer\Autoload\ComposerStaticInit7e421c277f7e8f810a19524f0d771cdb::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);
        }
    }

    // P2 将ClassLoader中的loadClass方法，注册为加载器
    $loader->register(true);

    if ($useStaticLoader) {
        $includeFiles = Composer\Autoload\ComposerStaticInit7e421c277f7e8f810a19524f0d771cdb::$files;
    } else {
        $includeFiles = require __DIR__ . '/autoload_files.php';
    }
    foreach ($includeFiles as $fileIdentifier => $file) {
        composerRequire7e421c277f7e8f810a19524f0d771cdb($fileIdentifier, $file);
    }

    return $loader;
}
```

在代码 P0 处，上来先实例化一个 `\Composer\Autoload\ClassLoader` 类，这个类里面维护了所有命名空间到类具体存放位置的映射关系。

接下来在 P1 处，根据 PHP 版本和运行环境，如是否运行在 HHVM 环境下，来区分如何向 ClassLoader 中载入映射关系。

autoload_static.php 文件中定义的映射关系有三种：

```php
public static $prefixLengthsPsr4 = array (
    'p' => 
    array (
        'phpDocumentor\\Reflection\\' => 25,
    ),
    'W' => 
    array (
        'Webmozart\\Assert\\' => 17,
    ),
    'S' => 
    array (
        'Symfony\\Polyfill\\Ctype\\' => 23,
    ),
    'R' => 
    array (
        'RefactoringGuru\\' => 16,
    ),
    'P' => 
    array (
        'Prophecy\\' => 9,
    ),
    'D' => 
    array (
        'Doctrine\\Instantiator\\' => 22,
        'DeepCopy\\' => 9,
    ),
);

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',
    ),
    'Webmozart\\Assert\\' => 
    array (
        0 => __DIR__ . '/..' . '/webmozart/assert/src',
    ),
    'Symfony\\Polyfill\\Ctype\\' => 
    array (
        0 => __DIR__ . '/..' . '/symfony/polyfill-ctype',
    ),
    'RefactoringGuru\\' => 
    array (
        0 => __DIR__ . '/../..' . '/',
    ),
    'Prophecy\\' => 
    array (
        0 => __DIR__ . '/..' . '/phpspec/prophecy/src/Prophecy',
    ),
    'Doctrine\\Instantiator\\' => 
    array (
        0 => __DIR__ . '/..' . '/doctrine/instantiator/src/Doctrine/Instantiator',
    ),
    'DeepCopy\\' => 
    array (
        0 => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy',
    ),
);

public static $classMap = array (
		'File_Iterator' => __DIR__ . '/..' . '/phpunit/php-file-iterator/src/Iterator.php',
		'File_Iterator_Facade' => __DIR__ . '/..' . '/phpunit/php-file-iterator/src/Facade.php',
		'File_Iterator_Factory' => __DIR__ . '/..' . '/phpunit/php-file-iterator/src/Factory.php',
		'PHPUnit\\Exception' => __DIR__ . '/..' . '/phpunit/phpunit/src/Exception.php',
    ...
);
```

classMap 是完整映射关系，prefixLengthsPsr4 和 prefixDirsPsr4 是当通过完整命名空间找不到时，通过在目标类名后加上 .php 再次寻找用。

到此，建立命名空间到类存放路径的关系已经完成了。

**第二步，如何找到类并加载**

在上面代码中，将 ClassLoader 的 loadClass 方法注册成加载器：

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

        return true;
    }
}

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

其中 findFile 方法，就是通过类名，去寻找文件实际的位置，如果找到了，就通过 includeFile 将文件加载进来。主要看看 findFile 中的逻辑：

```php
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 中取。如果取不到，则将目标类名追加 .php 后缀，去$prefixLengthsPsr4 和 $prefixDirsPsr4 中查找。

**第三步，如何加载全局函数**

```php
if ($useStaticLoader) {
    $includeFiles = Composer\Autoload\ComposerStaticInit7e421c277f7e8f810a19524f0d771cdb::$files;
} else {
    $includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
    composerRequire7e421c277f7e8f810a19524f0d771cdb($fileIdentifier, $file);
}

return $loader;
```

还是通过 autoload_static.php 中定义的数据去加载：

```php
// autoload_static.php
public static $files = array (
    '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
    '6124b4c8570aa390c21fafd04a26c69f' => __DIR__ . '/..' . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php',
);

// vendor/symfony/polyfill-ctype/bootstrap.php
if (!function_exists('ctype_alnum')) {
    function ctype_alnum($text) { return p\Ctype::ctype_alnum($text); }
}
if (!function_exists('ctype_alpha')) {
    function ctype_alpha($text) { return p\Ctype::ctype_alpha($text); }
}
```

至此 composer 自动加载的逻辑基本就过了一遍。

### 四、composer 的 ClassLoader 中的 classMap 是怎么生成出来的？

答案就在 composer 的源码中：https://github.com/composer/composer/blob/d0aac44ed210e13ec4a4370908a5b36553a2f16c/src/Composer/Autoload/AutoloadGenerator.php

扫描所有包中的类，然后生成一个 php 文件，例如：getStaticFile 方法

参考：
https://segmentfault.com/a/1190000014948542

PHP官网对类自动加载的说明：https://www.php.net/manual/zh/language.oop5.autoload.php