composer 自動加載原理分析

composer install 或 update

生成一個 vender 目錄,結(jié)構(gòu)如下

| 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. 首先執(zhí)行 autoload_real.php 文件中的類(類名為隨機生成為了保證唯一性)的靜態(tài)方法 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. 在執(zhí)行getLoader 方法的過程中,首先執(zhí)行 platform_check.php 檢查當前環(huán)境是否滿足條件

require __DIR__ . '/platform_check.php';

3. 然后執(zhí)行 spl_autoload_register(不懂這個方法可以點擊看官方文檔) 方法注冊當前類的 loadClassLoader 方法為自動加載的回調(diào)函數(shù)

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

當執(zhí)行下一行代碼 $loader = new \Composer\Autoload\ClassLoader 時會觸發(fā) loadClassLoader方法,然后會引入 ClassLoader.php 文件, 如下代碼

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

之后再取消這個注冊函數(shù),避免真正使用自動加載時受其影響

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

4. 根據(jù) $useStaticLoader 判斷是否使用靜態(tài)加載方式,通常為true,然后會引入 autoload_static.php 文件,此文件記錄了各個擴展包和根目錄中 composer.jsonautoload 屬性值psr-4,files,classmap的對應(yīng)關(guān)系, 分為 psr4, classmapfiles 3 類, 然后執(zhí)行 call_user_func 調(diào)用autoload_static.php 文件中類的靜態(tài)方法 getInitializer,將這些對應(yīng)關(guān)系綁定到 $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 屬性的數(shù)據(jù)結(jié)構(gòu)
  • files 格式為數(shù)組,記錄所有 files 對應(yīng)的文件

key 為唯一 hash id
value 為文件路徑

[
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
]
  • classmap 格式為數(shù)組,記錄當前項目(composer dump 之后的文件變動不算) classmap 文件夾中所有文件和 psr-4 對應(yīng)目錄下的所有文件

key 為類的完整命名
value 為文件路徑

[
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
]
  • psr4 分為2個,記錄所有 psr-4 對應(yīng)屬性, 一個按照首字母進行分組,一個記錄所有的命名空間映射

$prefixLengthsPsr4
key 為命名空間首字母
value 為數(shù)組記錄每個命名空間的長度

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

prefixDirsPsr4 key 為命名空間 value 為數(shù)組記錄文件所在路徑,之后會按照數(shù)組**先后順序**進行搜索,可以通過loader對象的 setPsr4方法重新設(shè)置或 addPsr4方法進行順序調(diào)整。

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

5. 之后執(zhí)行 $loader->register(true),開始進行真正的自動加載函數(shù)注冊

// 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;
}

其中關(guān)鍵為 fileFile 方法,findFile 分為 2 類,一類是 classmap 已經(jīng)提前收集到文件,這樣可以以 O(1) 的速度找到對應(yīng)文件, 另一類是尚未被 composer dump -o 的符合 psr4 自動加載規(guī)范的文件, 會執(zhí)行 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 加載規(guī)范的文件命名例如 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 對象供外部使用

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容