composer autoload源码分析

现在很多主流框架都用到了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流程都分析完毕,小弟才疏学浅,若有哪里不足,欢迎指正!!

猜你喜欢

转载自blog.51cto.com/chinalx1/2106113