透過源碼來了解 Composer Autoload 原理

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 所在的檔案引入