Laravel 源码阅读之一:Composer 自动加载分析

代码堆栈:入口文件public/index.phpvendor/autoload.phpvendor/composer/autoload_real.phpComposerAutoloaderInitxxx::getLoader()因原类名较长,类名后面的hash字串以‘xxx’代替
自动加载的初始化和自动加载函数的注册都将在getLoader方法中实现。

loadClassLoader方法

//该方法用于自动加载ClassLoader类
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}

getLoader方法代码及分析

public static function getLoader()
{
    if (null !== self::$loader) {  // 检查$loaders是否有值,有则直接返回
        return self::$loader;
    }

    /*
    |---------------------------------------------------------
    | 将 `ComposerAutoloaderInitxxx` 类的`loadClassLoader`方法注册为一个
    | `__autoload`函数的实现,无法注册成功则抛出错误,且添加到自动加载函数队
    | 列前面(即使用的类找不到时,自动调用`loadClassLoader`方法实现自动加载,
    | 具体实现见后面该方法分析)
    |---------------------------------------------------------
    */
    spl_autoload_register(array('ComposerAutoloaderInitxxx', 'loadClassLoader'), true, true);

    /*
    |---------------------------------------------------------
    | 这里实例化一个ClassLoader类,并赋值到$loader成员。
    | \Composer\Autoload\ClassLoader()按照字面的路径是找不到该类的,
    | 所以会触发`loadClassLoader`方法实现自动加载。
    |---------------------------------------------------------
    */
    self::$loader = $loader = new \Composer\Autoload\ClassLoader();

    // 得到 $loader 之后去掉前面注册的自动加载实现
    spl_autoload_unregister(array(ComposerAutoloaderInitxxx', 'loadClassLoader'));

    // 静态初始化只支持 PHP5.6 以上版本并且不支持 HHVM 虚拟机
    // zend_loader_file_encoded不清楚是什么,我的版本$useStaticLoader=true
    $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') 
                        && (!function_exists('zend_loader_file_encoded') 
                        || !zend_loader_file_encoded());
    if ($useStaticLoader) {
        require_once __DIR__ . '/autoload_static.php';

        //调用getInitializer方法
        // getInitializer见后面分析(B)
        call_user_func(\Composer\Autoload\ComposerStaticInitxxx::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);
        }
    }

    //register方法将classLoader方法加入自动加载函数队列
    //只要程序遇到不认识的类,就会使用该队列中的函数去查找类对应的文件
    //最后require进来,查找不到会做一个标记,下次查找时就可以直接识别该类
    //的文件是找不到的,直接返回false。后面展开分析该函数(C)
    $loader->register(true);

    //加载全局函数(分静态加载和非静态加载,结果是一样的)
    //$files成员变量是一个数组,包含'文件标识(哈希值)=>文件路径'的键值对
    if ($useStaticLoader) {
        $includeFiles = Composer\Autoload\ComposerStaticInitxxx::$files;
    } else {
        $includeFiles = require __DIR__ . '/autoload_files.php';
    }
    foreach ($includeFiles as $fileIdentifier => $file) {
        //composerRequirexxx在类的外部,封装了require函数,这样可以实现作用域隔离,
        //require进来的文件里面的变量,其作用域被包裹在`composerRequirexxx`中,
        //防止require进来的文件含有$this或self而产生调用混淆或错误,
        //而且该函数实现了require_once的效果,效率更高。分析见(A)部分。
        composerRequirexxx($fileIdentifier, $file);
    }

    //到这里,该方法返回一个`ClassLoader`类的实例,该实例的成员变量
    //(prefixLengthsPsr4、prefixDirsPsr4、fallbackDirsPsr4、prefixesPsr0等
    //都已经初始化,并且将`classLoader`方法添加到自动加载函数队列)
    return $loader;
}

loadClassLoader方法如何实现自动加载

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

当程序执行到self::$loader = $loader = new \Composer\Autoload\ClassLoader(); 时,\Composer\Autoload\ClassLoader 类并不能找到,这将去查找 __autoload 的实现函数队列,最终找到loadClassLoader 方法可以实现类的加载,并将 \Composer\Autoload\ClassLoade 作为参数转入。所以上面代码中,if条件成立,将 __DIR__ . '/ClassLoader.php' 文件导入。

(A) composerRequirexxx方法分析

function composerRequirexxx($fileIdentifier, $file)
{
    //文件标识为空才加载文件,实现了require_once的效果
    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
        //`$file`里面的变量,其作用域被包裹在 `composerRequirexxx`
        require $file;
        //将文件标识为已加载过的
        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
    }
}

(B) getInitializer方法分析

  public static function getInitializer(ClassLoader $loader)                                                    
  {                                                                                                             
      return \Closure::bind(function () use ($loader) {                                                         
          $loader->prefixLengthsPsr4 = ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::$prefixLengthsPsr4;  
          $loader->prefixDirsPsr4 = ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::$prefixDirsPsr4;        
          $loader->fallbackDirsPsr4 = ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::$fallbackDirsPsr4;    
          $loader->prefixesPsr0 = ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::$prefixesPsr0;            
          $loader->classMap = ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::$classMap;                    

      }, null, ClassLoader::class);                                                                             
  }                                                                                                             

Closure绑定的用法可参考:博客:PHP 闭包(Closure)
这里将一个闭包绑定到ClassLoader类,使得该类的私有成员变量可以被赋值,从而将这里的变量(顶级空间命名映射)搬到该类中。
该函数执行后得到的结果:
file
ClassLoader的成员变量实现了初始化,即它们保存了顶级命名空间到文件夹路径的映射。

(C) register方法分析

public function register($prepend = false)
{
    spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}

该方法将loadClass方法加入自动加载函数队列。loadClass方法:

public function loadClass($class)
{
    if ($file = $this->findFile($class)) {
        includeFile($file);

        return true;
    }
}

这里主要看findFile方法是怎么找到file的。接下来看findFile方法。

findFile方法分析

public function findFile($class)
{
    // class map lookup
    //如果classMap中有该类的文件映射,则直接返回对应的文件
    if (isset($this->classMap[$class])) {
        return $this->classMap[$class];
    }
    // isset($this->missingClasses[$class]表示
    //如果这个类已经被标为找不到的,则直接返回false
    //$this->classMapAuthoritative暂不清楚作用
    if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
        return false;
    }
    //暂不清楚是啥
    if (null !== $this->apcuPrefix) {
        $file = apcu_fetch($this->apcuPrefix.$class, $hit);
        if ($hit) {
            return $file;
        }
    }
    //使用psr4、psr0标准查找
    $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;
}

findFileWithExtension方法

private function findFileWithExtension($class, $ext)
{
    // PSR-4 lookup
    //将‘\’转为‘/’并加上后缀
    //以下分析,假设$class = 'phpDocumentor\Reflection\example' 
    //将'phpDocumentor\Reflection\example' 转为 'phpDocumentor/Reflection/example.php'
    $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;

    //取第一个字母‘p’
    $first = $class[0];

    //prefixLengthsPsr4数组中,有'P' => [ 'phpDocumentor\Reflection\' => 25 ]
    //这时,该条件为true(php数组key不区分大小写)
    //将命名空间用首字母归类,可以实现快速查找,如,这里如果没有找到‘P’作为开头的,
    //就可以不用继续查找,而是换别的查找方法。
    if (isset($this->prefixLengthsPsr4[$first])) {
        $subPath = $class;

        //计算字符串中最后一个‘\’的位置,并赋值给$lastPos,并判断是否存在‘\’
        //$lastPos = 24
        while (false !== $lastPos = strrpos($subPath, '\\')) {

            //从字符串开头算起,取$lastPos个字符
            //这里得到$subPath='phpDocumentor\Reflection'
            $subPath = substr($subPath, 0, $lastPos);

            $search = $subPath.'\\';
            //查找prefixDirsPsr4数组对应key是否有值,其key-value值如下:
                /*
                'phpDocumentor\Reflection\'=>
                ['/home/vagrant/code/Laravel/vendor/composer/../phpdocumentor/reflection-common/src',
                '/home/vagrant/code/Laravel/vendor/composer/../phpdocumentor/reflection-docblock/src',
                '/home/vagrant/code/Laravel/vendor/composer/../phpdocumentor/type-resolver/src'] 
                */
            if (isset($this->prefixDirsPsr4[$search])) {

               //$pathEnd = '/example.php'
                $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);

                //逐个检查prefixDirsPsr4['phpDocumentor\Reflection\']的文件路径是否包含需要的文件
                //例如,检查/home/vagrant/code/Laravel/vendor/composer/../phpdocumentor/reflection-common/src/example.php文件是否存在
                //如果存在,则返回文件路径
                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;
        }
    }

    // PSR-0 lookup
    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;
    }

    return false;
}

探索和疑问

  1. call_user_func是运行时才确定要调用的函数是什么,一般在不确定需要调用的函数是哪个的情况下使用,传入一个变量函数,该变量会根据程序的条件不同而不同(参考),效率比直接调用函数低。尝试将autoload_real.php中的这一行 : call_user_func(\Composer\Autoload\ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::getInitializer($loader));
    改成:
    \Composer\Autoload\ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::getInitializer($loader);,结果程序执行到getInitializer的return行直接跳出,$loader的成员变量并没有得到初始化,不知道为何会跟使用call_user_fun不一样。
  2. getInitializer可以改写成:

    public static function getInitializer(ClassLoader $loader)
    {
        return \Closure::bind(function () use ($loader) {
            $this->prefixLengthsPsr4 = ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::$prefixLengthsPsr4;
            $this->prefixDirsPsr4 = ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::$prefixDirsPsr4;
            $this->fallbackDirsPsr4 = ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::$fallbackDirsPsr4;
            $this->prefixesPsr0 = ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::$prefixesPsr0;
            $this->classMap = ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::$classMap;
    
        }, $loader, ClassLoader::class);
    }

    这里将闭包函数的$this绑定到ClassLoader类的实例$loader

  3. 全局函数在自动加载的时候全部加载进来,如果全局函数的数量较多,可能会对程序性能造成影响,所以,对于一些需要用到的函数,使用PHP 的Trait的来引入比较好。

小结

Composer自动加载所完成的工作有:

  • 实例化ClassLoader类,并初始化其成员变量,将顶级命名空间映射到文件夹路径的映射保存其中
  • loadClass方法加入自动加载函数队列,且该方法实现了classMap,psr4,psr0等方式的文件路径查找。当程序遇到不认识的类时,会调用该方法进行文件的加载
  • 实现全局函数的加载
    总的来说,Composer自动加载一方面接管了我们手动写一堆require或include的工作(想像一下,要require或include几千个文件会是什么样的情形),大大提高了开发效率和简洁代码;另一方面,Composer自动加载是使用到了类的时候才去查找并加载类的文件,实现了按需加载,节约程序开销,提高了程序的性能。
本作品采用《CC 协议》,转载必须注明作者和本文链接
Was mich nicht umbringt, macht mich stärker
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 2

file 我理解的是,没用call_user_func,返回的是匿名函数。用了这个call_user_func,就会执行调用匿名函数。

2年前 评论

\Composer\Autoload\ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::getInitializer($loader);,结果程序执行到 getInitializer 的 return 行直接跳出,$loader 的成员变量并没有得到初始化,不知道为何会跟使用 call_user_fun 不一样。

因为上述代码只是执行getInitializer方法,从,并没有执行getInitializer返回的匿名方法
按照你的思路,应该改成
\Composer\Autoload\ComposerStaticInit626d3a8755201dfffbe8d6a8cc4f8073::getInitializer($loader)();

1年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!