Composer 是 PHP 的套件管理工具,用於管理專案的相依關係,而 Composer 的自動載入功能 (autoload) 允許開發人員在不指定檔案路徑的情況下使用類別。本文將深入研究 Composer 自動載入的源碼,探討其運作原理和實踐細節。
本文章使用 composer 2.7.2
入口
/vendor/autoload.php
我們使用 composer 管理套件時,都需要先引入 vendor/autoload.php,因此我們先看到這個檔案
<?php
// autoload.php @generated by Composer
// 如果 php 是 5.6 以下 composer 就需要使用 2.2 本版
// 可以使用指令: composer self-update --2.2
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
// 引入 ComposerAutoloaderInit 類 (類的後面會帶上 hash)
require_once __DIR__ . '/composer/autoload_real.php';
// 使用 ComposerAutoloaderInit 類上的 getLoader() 靜態方法
return ComposerAutoloaderInitf70435b34cb6d4560a03336f9a2766b6::getLoader();
ComposerAutoloaderInit 類
/vendor/composer/autoload_real.php
在入口文件中,會發現引入了 /vendor/composer/autoload_real.php,裡面定義的是 ComposerAutoloaderInit 類,類名的後面會加上 hash
接下來可以跟著註解的編號讀
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInitf70435b34cb6d4560a03336f9a2766b6
{
private static $loader;
/**
* =========
* 2
* =========
* 從 getLoader() 可以知道這個方法被註冊為自動加載函數
*/
public static function loadClassLoader($class)
{
// 如果類名是 Composer\Autoload\ClassLoader,就去引入 /vendor/composer/ClassLoader.php
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* =========
* 1
* =========
* 從入口文件中,我們得知會去調用 getLoader() 這個靜態方法,因此先從這裡開始看
*/
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
// 先確認有沒有已經創建的 loader,如果有就直接用
if (null !== self::$loader) {
return self::$loader;
}
// 做一些版本檢測,可以先忽略
require __DIR__ . '/platform_check.php';
// 註冊一個自動加載函數,這個函數會在類被使用時自動去調用
// 這裡註冊此類上的 loadClassLoader() 方法
spl_autoload_register(array('ComposerAutoloaderInitf70435b34cb6d4560a03336f9a2766b6', 'loadClassLoader'), true, true);
// 造一個 \Composer\Autoload\ClassLoader 類
// 此時會自動使用剛剛註冊的 loadClassLoader() 函數
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
// 解除 loadClassLoader(),後續透過 $loader
spl_autoload_unregister(array('ComposerAutoloaderInitf70435b34cb6d4560a03336f9a2766b6', 'loadClassLoader'));
// 引入 ComposerStaticInit 類
require __DIR__ . '/autoload_static.php';
// 調用 ComposerStaticInit 類上的 getInitializer() 方法返回的函數
// 目的在於在 $loader 上加入 class 對應到的檔案
call_user_func(\Composer\Autoload\ComposerStaticInitf70435b34cb6d4560a03336f9a2766b6::getInitializer($loader));
// 調用 $loader 上的 register() 方法,相當於啟動 $loader
$loader->register(true);
return $loader;
}
}
ComposerStaticInit 類
/vendor/composer/autoload_static.php
靜態初始化,這隻檔案用來直接告訴 $loader 我們使用的 namespace 對應到的目錄
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInitf70435b34cb6d4560a03336f9a2766b6
{
// 用起始字母來排列 namespace 提高查詢效率
public static $prefixLengthsPsr4 = array (
'L' =>
array (
'Lynk\\TestPhp\\' => 13,
),
);
// namespace 實際對應到的目錄
public static $prefixDirsPsr4 = array (
'Lynk\\TestPhp\\' =>
array (
0 => __DIR__ . '/../..' . '/php',
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);
// 將這些映射關係都放到 $loader 上
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInitf70435b34cb6d4560a03336f9a2766b6::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInitf70435b34cb6d4560a03336f9a2766b6::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInitf70435b34cb6d4560a03336f9a2766b6::$classMap;
}, null, ClassLoader::class);
}
}
ClassLoader 類
/vendor/composer/ClassLoader.php
用來創建 $loader 的類,內部封裝真正使用的自動載入函數,也就是說,我們在使用類時,是透過 $loader 內的函數來引入對應的檔案
此部分,我們集中在依照 PSR-4 規範自動引入的幾個方法
register()
這個方法先前 /vendor/composer/autoload_real.php 檔案的最後可以發現有調用此方法,負責啟用 $loader,直接看到方法中的第一行
public function register($prepend = false)
{
// 註冊一個自動加載函數,這個函數會在類被使用時自動去調用
// 這裡註冊此類上的 loadClass() 方法
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
// ...
}
loadClass()
在 register() 方法中,可以看到我們將 loadClass() 註冊為自動載入方法,而此方法做以下兩件事
- 以類名找相對應的檔案
- 引入檔案
public function loadClass($class)
{
// $class 為類名,此處透過 findFile() 方法去找對應的檔案
// 如果找到了就引入該檔案
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
findFile()
從 loadClass() 方法中,我們知道是透過 findFile() 方法透過類名去找檔案。這個方法有多種找檔案的判斷,我們使用的 PSR-4 是在 findFileWithExtension() 方法中
public function findFile($class)
{
// ...
$file = $this->findFileWithExtension($class, '.php');
// ...
return $file;
}
findFileWithExtension()
此方法中,分別有使用 PSR-4 與 PSR-0 的找檔案方式,我們只關注 PSR-4 的部分
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
// PSR-4 的 namespace + 檔案路徑 + 副檔名
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
// 取得 namespace 的第一個字母,並對 prefixLengthsPsr4 陣列進行查找
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
// 以 \ 為間隔,持續縮短類名,直到找到正確的 namespace
// ex:
// Lynk\MyProject\Controller\UserController\
// Lynk\MyProject\Controller\
// Lynk\MyProject\
// => 找到 namespace
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
// 對比 prefixDirsPsr4 看是不是找到正確的 namespace
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;
}
}
}
}
}
// ...
}
結論
整個 autoload 的流程本質上為在 spl_autoload_register() 上註冊一個函數,這個函數會在我們使用 class 時自動被調用,而在函數中,會幫我們把當前 class 所在的檔案引入