Composer的自动加载详解

3,124 阅读3分钟

概述

众所周知composer是现代php项目的基石,composer并不是一款系统级别的包管理系统,而是一个基于php项目的包依赖管理工具,它允许你声明项目所依赖的代码库,它会在你的项目中安装这些依赖。这里我们不讲composer的具体使用细节,而是关注它自动加载方面的内容。

自动加载类型

composer提供了如下几种自动加载的规范(使用PSR-4规范):

  • PSR-0
  • PSR-4
  • classmap
  • files

更新autoload规则到对应的autoload配置文件使用 composer dump-autoload 命令。

PSR-0

现在这个标准已经过时了,这个标准主要考虑到了php5.2中 Code_Util_Score 这样的写法。如果代码结构如下:

├── honey
│   └── honey
│       ├── composer.json
│       └── lib
│           └── Code
│               └── Util
│                   └── Score.php

我们在项目的composer.json文件里进行autoload的声明:

"autoload":{
    "psr-0": {
        "Code" : "vendor/honey/honey/lib/"
    }
}

然后在项目的根目录下执行如下命令来更新自动加载配置:

$ composer dump-autoload

然后查看vendor/composer/autoload_namespaces.php

// autoload_namespaces.php @generated by Composer

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

return array(
    'Code' => array($vendorDir . '/honey/honey/lib'),
);

可以看到以 Code 为前缀的类名在vendor/honey/honey/lib 目录下寻找,在加载 Code_Util_Score 这个类时会把下划线转化成目录分隔符。

PSR-4

在PSR-4里边需要定义一个命名空间前缀到路径的映射(相对于包的根目录),如果命名空间前缀Foo\指向一个文件目录src/,当自动加载一个类时,比如Foo\Bar\Baz类,那么这个类的路径为 src/Bar/Baz.php,命名空间前缀可以不在路径之中。在composer.json中的命名空间必须以\结尾,以避免名字冲突,示例如下:

{
    "autoload": {
        "psr-4": {
            "Monolog\\": "src/",
            "Vendor\\Namespace\\": ""
        }
    }
}

如果想把多个目录下的文件放到同一个命名空间前缀下,可以用如下写法:

{
    "autoload": {
        "psr-4": { "Monolog\\": ["src/", "lib/"] }
    }
}
classmap

classmap引用的所有组合都会在install/update过程中生成,并存储到vendor/composer/autoload_classmap.php文件中。这个map是经过扫描指定目录(同样支持直接精确到文件)中所有的 .php 和 .inc 文件里内置的类而得到的。你可以用classmap生成支持自定义加载的不遵循PSR-0/4规范的类库。要配置它指向需要的目录,以便能够准确搜索到类文件。

{
    "autoload": {
        "classmap": ["src/", "lib/", "Something.php"]
    }
}
files

如果你想要明确的指定,在每次请求时都要载入某些文件,那么你可以使用files autoloading。通常作为函数库的载入方式(而非类库)。files引用的所有集合都会在install/update过程中生成,并存储到vendor/composer/autoload_files.php文件中。

{
    "autoload": {
        "files": ["src/MyLibrary/functions.php"]
    }
}

自动加载原理

在我们的项目根目录下创建一个composer.json,在这个文件里声明我们的依赖:

{
    "require": {
        "workerman/workerman": "^3.3"
    }
}

然后执行composer install就可以安装该依赖,composer会把依赖安装到vendor目录下并在vendor目录生成一个autoload.php文件,在项目中引入该autoload文件就可以使用vendor下面的库了。

require_once 'vendor/autoload.php';

use Workerman\Worker;

$worker = new Worker('http://0.0.0.0:8080');

$worker->onMessage = function($connection, $data)
{
    $connection->send("Hello World");
};

// 运行worker
Worker::runAll();

接下来我们来看一下composer自动加载的奥秘吧。

vendor/autoload.php

这个文件是自动加载的入口文件,打开该文件内容如下:

// autoload.php @generated by Composer

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

return ComposerAutoloaderInit6b02afbbb0f09c7f4f45234765981b94::getLoader();

可以看到它引入了vendor/composer/autoload_real.php文件,调用了自动加载类的getLoader方法并将结果返回。

vendor/composer/autoload_real.php
// autoload_real.php @generated by Composer

class ComposerAutoloaderInit6b02afbbb0f09c7f4f45234765981b94
{
    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('ComposerAutoloaderInit6b02afbbb0f09c7f4f45234765981b94', 'loadClassLoader'), true, true);
        self::$loader = $loader = new \Composer\Autoload\ClassLoader();
        spl_autoload_unregister(array('ComposerAutoloaderInit6b02afbbb0f09c7f4f45234765981b94', 'loadClassLoader'));

        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION');
        if ($useStaticLoader) {
            require_once __DIR__ . '/autoload_static.php';

            call_user_func(\Composer\Autoload\ComposerStaticInit6b02afbbb0f09c7f4f45234765981b94::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\ComposerStaticInit6b02afbbb0f09c7f4f45234765981b94::$files;
        } else {
            $includeFiles = require __DIR__ . '/autoload_files.php';
        }
        foreach ($includeFiles as $fileIdentifier => $file) {
            composerRequire6b02afbbb0f09c7f4f45234765981b94($fileIdentifier, $file);
        }

        return $loader;
    }
}

function composerRequire6b02afbbb0f09c7f4f45234765981b94($fileIdentifier, $file)
{
    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
        require $file;

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

下面是代码分析:

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

    //注册一个自动加载函数到__autoload函数栈中
    spl_autoload_register(array('ComposerAutoloaderInit6b02afbbb0f09c7f4f45234765981b94', 'loadClassLoader'), true, true);
    //实例化一个自动加载类并存储到静态变量里
    self::$loader = $loader = new \Composer\Autoload\ClassLoader();
    //注销这个自动加载函数
    spl_autoload_unregister(array('ComposerAutoloaderInit6b02afbbb0f09c7f4f45234765981b94', 'loadClassLoader'));

    //版本判断看使用哪些自动加载的配置文件
    $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION');
    if ($useStaticLoader) {
        require_once __DIR__ . '/autoload_static.php';

        call_user_func(\Composer\Autoload\ComposerStaticInit6b02afbbb0f09c7f4f45234765981b94::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);
        }
    }

    //调用加载器的注册方法注册自动加载函数include文件
    $loader->register(true);

    //加载一些函数文件
    if ($useStaticLoader) {
        $includeFiles = Composer\Autoload\ComposerStaticInit6b02afbbb0f09c7f4f45234765981b94::$files;
    } else {
        $includeFiles = require __DIR__ . '/autoload_files.php';
    }
    foreach ($includeFiles as $fileIdentifier => $file) {
        composerRequire6b02afbbb0f09c7f4f45234765981b94($fileIdentifier, $file);
    }

    return $loader;
}

这就是composer自动加载的过程,这里涉及到的文件如下所示:

  • vendor/autoload.php 自动加载入口文件
  • vendor/composer/autoload_real.php 自动加载核心文件
  • vendor/composer/ClassLoader.php 自动加载类具体实现文件
  • vendor/composer/autoload_static.php 所有的自动加载配置
  • vendor/composer/autoload_classmap.php classmap自动加载配置
  • vendor/composer/autoload_namespaces.php PSR0自动加载配置
  • vendor/composer/autoload_psr4.php PSR4自动加载配置
  • vendor/composer/autoload_files.php files自动加载配置

这就是composer整个自动加载的流程。