美文网首页
composer 自动加载原理分析

composer 自动加载原理分析

作者: sorry510 | 来源:发表于2022-01-31 20:32 被阅读0次

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 对象供外部使用

相关文章

  • composer 自动加载原理分析

    composer install 或 update 生成一个 vender 目录,结构如下 使用自动加载 自动加载...

  • composer

    composer composer命令 包的版本号 composer自动加载composer提供了以下几种自动加载...

  • PHP Composer详解

    PHP composer php composer的作用 composer是如何实现类的自动加载 composer...

  • SpringBoot自动配置原理

    自动配置原理 分析自动配置原理 SpringBoot启动的时候加载主配置类,开启了自动配置功能 @EnableAu...

  • Laravel5.4 启动探索

    index.php 入口文件 自动加载初窥 自动加载最主要代码(来自vendor/composer目录下的自动加载...

  • 知识点汇总

    Laravel框架关键技术 熟悉SOLID原则 了解部分设计模式 熟悉Composer的自动加载原理和造轮子规范 ...

  • 【Composer】自动加载 autoload

    PHP类库的自动加载主要是由composer来实现的。 项目安装composer之后(composer insta...

  • composer 的常用命令

    更新composer self-update 自动加载文件composer dump-autoload 使用阿里云...

  • composer自动加载

    理解自动加载函数的原理 在实例话一个类时,如有全局的自动加载函数,则会将类的命名空间及类名传入到加载函数,然后由加...

  • composer自动加载

    1.首先创建一个空项目,执行composer init生成一个composer.json文件,然后compose...

网友评论

      本文标题:composer 自动加载原理分析

      本文链接:https://www.haomeiwen.com/subject/zvmhkrtx.html