composer 自动加载

󰃭 2017-07-18

composer 自动加载

入口

在laravel项目中,所有的依赖包都存在于vendor目录下,对应的类自动加载源码也存在于vendor目录中(由入口文件public/index.php可追踪到),首先打开目录我们可以在最外层找到 autoload.php 对应代码如下

// 引入了真正的aotuload
require_once __DIR__ . '/composer/autoload_real.php';
// 调用了autoload核心函数
return ComposerAutoloaderInit9a624ef1199d9f899a6118c386c86aa3::getLoader();

核心

追踪到autoload.php时,可以找到自动加载最核心的代码 getLoader ,可以通过以下问题来理清自动加载的流程:

自动加载的规则

vendor中的每个依赖都有composer.json文件,其中 autoload 这个字段告诉composer该依赖遵循了php哪种自动加载的规则,使得composer可以将它的规则写入对应的文件中以实现自动加载。目前composer中支持四种规则分别为 psr-0psr-4classmapfiles

源码分析

  • classmap
// 以hamcrest/hamcrest-php为例

// composer.json 中定义了该插件使用了classmap的规则
{
	"autoload": {
		"classmap": ["hamcrest"],
		"files": ["hamcrest/Hamcrest.php"]
	},
}

// 目录结构
vendor
└── hamcrest
	└── hamcrest-php
		├──hamcrest
			├──Hamcrest
			    ├── Arrays
			       ├── IsArray.php
			       ├── IsArrayContaining.php
			       ├── IsArrayContainingInAnyOrder.php
...

// 对应 autoload_static.php中展现 
// classmap
public static $classMap = array (
	'Hamcrest\\Arrays\\IsArray' => __DIR__ . '/..' . '/hamcrest/hamcrest-php/hamcrest/Hamcrest/Arrays/IsArray.php',
    'Hamcrest\\Arrays\\IsArrayContaining' => __DIR__ . '/..' . '/hamcrest/hamcrest-php/hamcrest/Hamcrest/Arrays/IsArrayContaining.php',
    'Hamcrest\\Arrays\\IsArrayContainingInAnyOrder' => __DIR__ . '/..' . '/hamcrest/hamcrest-php/hamcrest/Hamcrest/Arrays/IsArrayContainingInAnyOrder.php',
)
  • psr-0
// 以jakub-onderka/php-console-color为例

// composer.json 中定义了该插件使用了psr-0的规则
{
	"autoload": {
        "psr-0": {"JakubOnderka\\PhpConsoleColor": "src/"}
    }
}

// 目录结构
vendor
└── jakub-onderka
	└── php-console-color
	    ├── src
	       └── JakubOnderka
	           └── PhpConsoleColor
	               └── ConsoleColor.php
	    └── tests
	        └── JakubOnderka
	            └── PhpConsoleColor
	                └── ConsoleColorTest.php

// 对应 autoload_static.php中展现 
// psr-0
public static $prefixesPsr0 = array (
	'J' => 
        array (
            'JakubOnderka\\PhpConsoleColor' => 
            array (
                0 => __DIR__ . '/..' . '/jakub-onderka/php-console-color/src',
            ),
        ),
)
// classmap
public static $classMap = array (
	'JakubOnderka\\PhpConsoleColor\\ConsoleColor' => __DIR__ . '/..' . '/jakub-onderka/php-console-color/src/JakubOnderka/PhpConsoleColor/ConsoleColor.php',
)
  • psr-4
// 以mongodb/mongodb为例

// composer.json 中定义了该插件使用了psr-0的规则
{
	"autoload": {
        "psr-4": { "MongoDB\\": "src/" },
        "files": [ "src/functions.php" ] // files 为全局引用文件注册
    }
}

// 目录结构
vendor
└── mongodb
	└── mongodb
	    ├── src
	       └── ClassName.php
	    └── tests
	        └── Test.php

// 对应 autoload_static.php中展现 
// psr-4
public static $prefixLengthsPsr4 = array(
	array (
	    'MongoDB\\' => 8,
    ),
)
public static $prefixDirsPsr4 = array (
	array (
	   0 => __DIR__ . '/..' . '/mongodb/mongodb/src',
   ),
)
// classmap
public static $classMap = array (
	'MongoDB\\Client' => __DIR__ . '/..' . '/mongodb/mongodb/src/Client.php',
)
  • file

// 单个file的引入 在之前psr-4中出现过一个例子,在这里还是使用它
// composer.json 
{
	"autoload": {
        "psr-4": { "MongoDB\\": "src/" },
        "files": [ "src/functions.php" ] // files 为全局引用文件注册
    }
}

// autoload_static.php
public static $files = array (
	'3a37ebac017bc098e9a86b35401e7a68' => __DIR__ . '/..' . '/mongodb/mongodb/src/functions.php',
)

总结

file: 目的是全局引入单个文件,composer中以json数组形式申明,罗列需要引入的文件

classmap:递归给定目录下所有文件/文件夹,对应目录名为命名空间,composer中以json数组形式申明,罗列需要递归的目标目录

psr-0和psr-4: php规范,规定了命名空间的规范,psr-4的目录层级明显较psr-0更简洁,composer中以json对象的形式申明,key值为命名空间,value值指定了文件所在的相对目录由于psr规范的命名空间解析使用时还需额外判断文件是否存在等才做,增加了io消耗,顾所有psr规则的命名空间会生成一个classmap项储存,优先获取classmap中的对应关系,若匹配不到才会根据psr规则获取对应文件

自动加载函数

源码分析

在获取到所有自动加载类与文件夹对应的数组后(储存于ClassLoad的私有变量中),代码执行到 $loader->register(true); 执行完这句代码,所有的可自动加载的类通过 spl_autoload_register() 注册

下面通过跟踪register学习注册的过程composer做了些什么


// 以下代码位于vendeor/composer/ClassLoader.php中
// register函数中只处理classmap,psr-0,psr-4规则的类
public function register($prepend = false)
{
	spl_autoload_register(array($this, 'loadClass'), true, $prepend); // 指定了自动加载函数
}

// 加载类之前需要检查文件是否存在
// 其中includeFile()较为简单,重点在findFile()
public function loadClass($class)
{
	if ($file = $this->findFile($class)){
      includeFile($file);
      return true;
    }
}

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

public function findFile($class) {
	// 首先先从classmap中寻找是否存在类
    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;
        }
     }
     // 以上方式查找失败,遵循psr-0,psr-4寻找对应文件
     $file = $this->findFileWithExtension($class, '.php');
     // 没有命中,并且定义了hhvm版本号,查找.hh文件
     if (false === $file && defined('HHVM_VERSION')) {
     	$file = $this->findFileWithExtension($class, '.hh');
     }
     // 命中文件后存入缓存,下次查找文件可从缓存中获取
     if (null !== $this->apcuPrefixF) {
     	apcu_add($this->apcuPrefix.$class, $file);
     }
     // 命中失败存在missingClasses数组中,下次查找直接返回失败
     if (false === $file) {
     	// Remember that this class does not exist.
        	$this->missingClasses[$class] = true;
        }
        return $file;
     }
 }

以上过程只覆盖了classmap, psr-0, psr-4,在之前介绍的四种规则种的file并没有在这里处理,因为class文件,在使用中才会动态引入,file会在代码执行之前完成所有文件引入,所以file文件中通常是一些全局函数,变量和一些必须的前置处理。


$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
// 判断是否使用静态调用
if ($useStaticLoader) {
    // 从autoload_static.php中获取files
    $includeFiles = Composer\Autoload\ComposerStaticInit9a624ef1199d9f899a6118c386c86aa3::$files;
 } else {
    // 从autolaod_files.php中获取files
    $includeFiles = require __DIR__ . '/autoload_files.php';
 }
 
foreach ($includeFiles as $fileIdentifier => $file) {
	composerRequire9a624ef1199d9f899a6118c386c86aa3($fileIdentifier, $file);
}

function composerRequire9a624ef1199d9f899a6118c386c86aa3($fileIdentifier, $file){
    // 已经require的文件在全局中标识,以防多次引入
    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
    	require $file;
        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
        }
    }
}

总结

自动加载文件分使用时加载和立即加载,classmap,psr-0,psr-4规则的文件在使用时才会动态引入,file文件会在getload时全局引入。使用时引入的文件中,引入classmap中记载的命名空间最省资源。

没有在classmap中找到的命名空间,无论是否最后找到对应文件都有所记录已节省下次搜索的时间。(没找到的记录在ClassLoader中的 $missingClasses 数组里,搜索到的记录在apcu中留档)