composer 自动加载原理分析

composer install 或 update

生成一个 vender 目录,结构如下

| vender
--| composer
----| autoload_classmap.php
----| autoload_files.php
----| autoload_namespaces.php
----| autoload_psr4.php
----| autoload_real.php
----| autoload_static.php
----| ClassLoader.php
----| installed.json
----| installed.php
----| InstalledVersions.php
----| LICENSE
----| platform_check.php
| autoload.php // 入口文件

使用自动加载

// test.php 在根目录下
define('BASE_PATH', dirname(__FILE__));
$loader = require BASE_PATH . './vendor/autoload.php';

自动加载原理分析

1. 首先执行 autoload_real.php 文件中的类(类名为随机生成为了保证唯一性)的静态方法 getLoader

/**
  * @return \Composer\Autoload\ClassLoader
  */
public static function getLoader()
{
   if (null !== self::$loader) {
       return self::$loader;
   }

   require __DIR__ . '/platform_check.php';

   spl_autoload_register(array('ComposerAutoloaderInited8b61ae552a5f53651efbac4ac65b0c', 'loadClassLoader'), true, true);
   self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
   spl_autoload_unregister(array('ComposerAutoloaderInited8b61ae552a5f53651efbac4ac65b0c', 'loadClassLoader'));

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

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

   return $loader;
}

2. 在执行getLoader 方法的过程中,首先执行 platform_check.php 检查当前环境是否满足条件

require __DIR__ . '/platform_check.php';

3. 然后执行 spl_autoload_register(不懂这个方法可以点击看官方文档) 方法注册当前类的 loadClassLoader 方法为自动加载的回调函数

spl_autoload_register(array('ComposerAutoloaderInited8b61ae552a5f53651efbac4ac65b0c', 'loadClassLoader'), true, true);

当执行下一行代码 $loader = new \Composer\Autoload\ClassLoader 时会触发 loadClassLoader方法,然后会引入 ClassLoader.php 文件, 如下代码

// autoload_real.php
public static function loadClassLoader($class)
{
    if ('Composer\Autoload\ClassLoader' === $class) {
        require __DIR__ . '/ClassLoader.php';
    }
}

之后再取消这个注册函数,避免真正使用自动加载时受其影响

spl_autoload_unregister(array('ComposerAutoloaderInited8b61ae552a5f53651efbac4ac65b0c', 'loadClassLoader'));

4. 根据 $useStaticLoader 判断是否使用静态加载方式,通常为true,然后会引入 autoload_static.php 文件,此文件记录了各个扩展包和根目录中 composer.jsonautoload 属性值psr-4,files,classmap的对应关系, 分为 psr4, classmapfiles 3 类, 然后执行 call_user_func 调用autoload_static.php 文件中类的静态方法 getInitializer,将这些对应关系绑定到 $loader 对象上,代码如下

// autoload_real.php
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
    require __DIR__ . '/autoload_static.php';
    call_user_func(\Composer\Autoload\ComposerStaticInited8b61ae552a5f53651efbac4ac65b0c::getInitializer($loader));
}
// autoload_static.php
public static function getInitializer(ClassLoader $loader)
{
    return \Closure::bind(function () use ($loader) {
        $loader->prefixLengthsPsr4 = ComposerStaticInited8b61ae552a5f53651efbac4ac65b0c::$prefixLengthsPsr4;
        $loader->prefixDirsPsr4 = ComposerStaticInited8b61ae552a5f53651efbac4ac65b0c::$prefixDirsPsr4;
        $loader->classMap = ComposerStaticInited8b61ae552a5f53651efbac4ac65b0c::$classMap;
    }, null, ClassLoader::class);
}
autoload 属性的数据结构
  • files 格式为数组,记录所有 files 对应的文件

key 为唯一 hash id
value 为文件路径

[
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
]
  • classmap 格式为数组,记录当前项目(composer dump 之后的文件变动不算) classmap 文件夹中所有文件和 psr-4 对应目录下的所有文件

key 为类的完整命名
value 为文件路径

[
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
]
  • psr4 分为2个,记录所有 psr-4 对应属性, 一个按照首字母进行分组,一个记录所有的命名空间映射

$prefixLengthsPsr4
key 为命名空间首字母
value 为数组记录每个命名空间的长度

[
    'S' => 
        [
            'Symfony\\Polyfill\\Php80\\' => 23,
            'Symfony\\Polyfill\\Mbstring\\' => 26,
            'Symfony\\Component\\VarDumper\\' => 28,
            'Simple\\Test\\' => 12,
        ]
]

prefixDirsPsr4 key 为命名空间 value 为数组记录文件所在路径,之后会按照数组**先后顺序**进行搜索,可以通过loader对象的 setPsr4方法重新设置或 addPsr4方法进行顺序调整。

[
     'Symfony\\Polyfill\\Php80\\' => 
        array (
            0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
        ),
]

5. 之后执行 $loader->register(true),开始进行真正的自动加载函数注册

// ClassLoader.php
public function register($prepend = false)
{
    spl_autoload_register(array($this, 'loadClass'), true, $prepend);

    if (null === $this->vendorDir) {
        return;
    }

    if ($prepend) {
        self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
    } else {
        unset(self::$registeredLoaders[$this->vendorDir]);
        self::$registeredLoaders[$this->vendorDir] = $this;
    }
}
// ClassLoader.php
public function loadClass($class)
{
    if ($file = $this->findFile($class)) {
        includeFile($file);

        return true;
    }

    return null;
}

其中关键为 fileFile 方法,findFile 分为 2 类,一类是 classmap 已经提前收集到文件,这样可以以 O(1) 的速度找到对应文件, 另一类是尚未被 composer dump -o 的符合 psr4 自动加载规范的文件, 会执行 findFileWithExtension 方法寻找文件所在 path

// ClassLoader.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;
} 
// ClassLoader.php
private function findFileWithExtension($class, $ext)
{
    // PSR-4 lookup
    $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;

    $first = $class[0];
    if (isset($this->prefixLengthsPsr4[$first])) {
        $subPath = $class;
        while (false !== $lastPos = strrpos($subPath, '\\')) {
            $subPath = substr($subPath, 0, $lastPos);
            $search = $subPath . '\\';
            if (isset($this->prefixDirsPsr4[$search])) {
                $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
                foreach ($this->prefixDirsPsr4[$search] as $dir) {
                    if (file_exists($file = $dir . $pathEnd)) {
                        return $file;
                    }
                }
            }
        }
    }

    // PSR-4 fallback dirs
    foreach ($this->fallbackDirsPsr4 as $dir) {
        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
            return $file;
        }
    }
    return false;
}

ps: $this->fallbackDirsPsr4 是用于不符合 psr4 加载规范的文件命名例如 Test.php 文件的命名空间 namespace Foo\Test, 文件路径为 Bar/Foo/Test.php,可以使用 $loaderaddPsr4 方法进行添加, $loader->addPsr4(null, BASE_PATH . './Bar');

public function addPsr4($prefix, $paths, $prepend = false)
{
    if (!$prefix) {
        // Register directories for the root namespace.
        if ($prepend) {
            $this->fallbackDirsPsr4 = array_merge(
                (array) $paths,
                $this->fallbackDirsPsr4
            );
        } else {
            $this->fallbackDirsPsr4 = array_merge(
                $this->fallbackDirsPsr4,
                (array) $paths
            );
        }
    } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
        // Register directories for a new namespace.
        $length = strlen($prefix);
        if ('\\' !== $prefix[$length - 1]) {
            throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
        }
        $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
        $this->prefixDirsPsr4[$prefix] = (array) $paths;
    } elseif ($prepend) {
        // Prepend directories for an already registered namespace.
        $this->prefixDirsPsr4[$prefix] = array_merge(
            (array) $paths,
            $this->prefixDirsPsr4[$prefix]
        );
    } else {
        // Append directories for an already registered namespace.
        $this->prefixDirsPsr4[$prefix] = array_merge(
            $this->prefixDirsPsr4[$prefix],
            (array) $paths
        );
    }
}

6. 之后将所有 composer.json 文件中 autoload 属性中的 files 所记录的文件依次 require,

// autoload_real.php
if ($useStaticLoader) {
    $includeFiles = Composer\Autoload\ComposerStaticInited8b61ae552a5f53651efbac4ac65b0c::$files; // 使用 composer dump 缓存后的文件
} else {
    $includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
    composerRequireed8b61ae552a5f53651efbac4ac65b0c($fileIdentifier, $file);
}

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

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

7. 最后返回 $loader 对象供外部使用

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容