现在很多主流框架都用到了composer,包管理实在是方便。现在我就以yii2来举例追踪一遍composer autoload流程
第一步上yii2的web/index.php(入口文件)
<?php
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');
require __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';
$config = require __DIR__ . '/../config/web.php';
(new yii\web\Application($config))->run();
看到第五行,有引入vendor/autoload.php,于是我打开这个文件:
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitc9edea7c33c2cce60a5a807424f71d3e::getLoader();
打开这个vendor/composer/autoload_real.php,找到ComposerAutoloaderInitc9edea7c33c2cce60a5a807424f71d3e这个类的getLoader方法
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInitc9edea7c33c2cce60a5a807424f71d3e', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInitc9edea7c33c2cce60a5a807424f71d3e', 'loadClassLoader'));
//psr0 分析见解释1-1
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
//psr4(这项最重要) //分析见解释1-2
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
//分析见解释1-3
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
$loader->register(true); //分析见解释1-4
//分析见解释1-5
$includeFiles = require __DIR__ . '/autoload_files.php';
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequired991c576f7683adde3c22cdd0fe508de($fileIdentifier, $file);
}
return $loader;
}
解释1-1
首先来看看这个autoload_namespaces.php
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Imagine' => array($vendorDir . '/imagine/imagine/lib'),
'HTMLPurifier' => array($vendorDir . '/ezyang/htmlpurifier/library'),
'Diff' => array($vendorDir . '/phpspec/php-diff/lib'),
);
很明显仅仅返回一个数组
再看看这语句含义
$loader->set($namespace, $path);
追踪进去,调用的是ClassLoader的set方法:
public function set($prefix, $paths)
//以$prefix='Imagine', $paths = array($vendorDir . '/imagine/imagine/lib')来举例
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
//得到$this->prefixesPsr0['I']['Imagine'] = array($vendorDir . '/imagine/imagine/lib')
}
}
解释1-2
打开vendor/composer/autoload_psr4.php
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'yii\\swiftmailer\\' => array($vendorDir . '/yiisoft/yii2-swiftmailer'),
'yii\\redis\\' => array($vendorDir . '/yiisoft/yii2-redis'),
'yii\\mongodb\\' => array($vendorDir . '/yiisoft/yii2-mongodb'),
);
这个文件看过来,发现和psr0的namespace.php很像,只是这里加了命名空间
和psr0一样,我们来看看这行
$loader->setPsr4($namespace, $path);
打开ClassLoader的setPsr4方法
public function setPsr4($prefix, $paths)
//以$prefix='yii\\mongodb\\', $paths = array($vendorDir . '/yiisoft/yii2-mongodb')来举例
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
//字符串'yii\\mongodb\\'长度为12(注意这里长度计算实际为yii\mongodb\)
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
//$this->prefixLengthsPsr4['y']['yii\\mongodb\\'] = 12;
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
//$this->prefixLengthsPsr4['y']['yii\\mongodb\\'] = array($vendorDir . '/yiisoft/yii2-mongodb')
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
解释1-3(类名和类文件具体路径直接映射表)
打开autoload_classmap.php
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'File_Iterator' => $vendorDir . '/phpunit/php-file-iterator/src/Iterator.php',
'File_Iterator_Facade' => $vendorDir . '/phpunit/php-file-iterator/src/Facade.php',
'File_Iterator_Factory' => $vendorDir . '/phpunit/php-file-iterator/src/Factory.php',
);
经过上面分析,现在我们直接来看ClassLoader的addClassMap方法:
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/*
方法执行完
$this->classMap = [
'File_Iterator' => $vendorDir . '/phpunit/php-file-iterator/src/Iterator.php',
'File_Iterator_Facade' => $vendorDir . '/phpunit/php-file-iterator/src/Facade.php',
'File_Iterator_Factory' => $vendorDir . '/phpunit/php-file-iterator/src/Factory.php',
];
*/
解释1-4
关键代码$loader->register(true);
打开loader的register方法
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
看完实际上就是通过当前类的loadClass注册了,假如new yii\mongodb\Connection找不到的时候就会调用ClassLoader的loadClass方法,于是我们看看loadClass方法
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
看完继续打开这个findFile方法
public function findFile($class)
{
// work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
if ('\\' == $class[0]) {
$class = substr($class, 1);
}
// 这里解释1-3解释过了
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
//$this->classMapAuthoritative默认为false
if ($this->classMapAuthoritative) {
return false;
}
$file = $this->findFileWithExtension($class, '.php');
// 如果运行在HHVM
if ($file === null && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if ($file === null) {
//记住这个类不存在
return $this->classMap[$class] = false;
}
return $file;
}
这个方法都好理解,假如是new yii\mongodb\Connection肯定会进入到这行
$file = $this->findFileWithExtension($class, '.php'); //实际上$this->findFileWithExtension('yii\\mongodb\\Connection.php')
于是我们继续读findFileWithExtension方法
private function findFileWithExtension($class, $ext) //举例$class = 'yii\\mongodb\\Connection', $ext = '.php'
{
// PSR-4寻找
//会得到$logicalPathPsr4 = 'yii/mongodb/Connection.php';
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
//由于我们之前setPsr4设置过此数组于是找到了
if (isset($this->prefixLengthsPsr4[$first])) {
//$length 为字符串'yii\\mongodb\\'长度
foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) {
if (0 === strpos($class, $prefix)) {
foreach ($this->prefixDirsPsr4[$prefix] as $dir) {
//$dir为autoload_psr4.php映射的路径,substr($logicalPathPsr4, $length)
//刚好可以得到Connection.php
//最终得到$file完整路径
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0同理
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;
}
}
通过这个方法找到类文件的完整路径,最后只需要include($file)即可,到此composer autoload自动加载基本完毕
解释1-5
我们还是先看看autoload_files.php文件是什么鬼
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'2cffec82183ee1cea088009cef9a6fc3' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php',
'2c102faa651ef8ea5874edb585946bce' => $vendorDir . '/swiftmailer/swiftmailer/lib/swift_required.php',
);
再分析调用语句
composerRequired991c576f7683adde3c22cdd0fe508de($fileIdentifier, $file);
再看看这个方法:
function composerRequired991c576f7683adde3c22cdd0fe508de($fileIdentifier, $file)
/*
$fileIdentifier = '2cffec82183ee1cea088009cef9a6fc3'
$file = $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php'
*/
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}
到此,整个composer autoload流程都分析完毕,小弟才疏学浅,若有哪里不足,欢迎指正!!