再看 Composer 自動載入原始碼

悲劇不上演發表於2019-03-30
  1. 請大家先看一下 你生產環境的 Composer 是這樣嗎? 中的自動載入部分
  2. 請大家先大概看一次原始碼在閱讀該文章,這些都是我在看的過程產生的疑問一步一步查到,可以為大家節省一些時間。

自動載入規範

PSR 是由 PHP FIG 組織制定的 PHP 規範,是 PHP 開發的實踐標準。Composer 自動載入規範也遵循該規則。

PSR-0 與 PSR-4 規範

PSR-0 與 PSR-4 區別

下化線區別

PSR-0 : 類名稱中的每個 _ 字元也會被轉換成資料夾路徑分隔符,而名稱空間中的 _ 字元則是無特殊含義的。

PSR-4 : 無等數含義

對映目錄區別

"autoload": {
    "psr-0": {
        "Cx\\": "prs0/"
    },
    "psr-4": {
        "App\\": "app/"
    }
}

假設檔案的根目錄為 /path/to/project

規範 名稱空間類 檔案目錄
PSR-0 Cx\My\Big_Dog /path/to/project/psr0/Cx/My/Big/Dog.php
PSR-4 App\My\Big_Dog /path/to/project/app/My/Big_Dog.php

目錄

下面描述的是 vendor/composer 目錄下的檔案

檔案 作用
autoload_classmap.php 所有名稱空間包含的類(不一定會有,看是否執行了 composer dump-autoload -o) (類檔案名稱空間與檔案位置的對應陣列)
autoload_files.php 用於載入全域性函式的檔案(靜態檔案陣列)
autoload_namespaces.php 符合 PSR0 標準的自動載入檔案(PRS-0 名稱空間 與位置對應陣列)
autoload_psr4.php 符合 PSR4 標準的自動載入檔案 (PRS-4 名稱空間 與位置對應陣列)
autoload_real.php 自動載入功能的引導類。(入口
autoload_static.php 頂級名稱空間與檔案路徑對應 (輔助作用,給ClassLoader類屬性初始化)
ClassLoader.php composer 載入類。(核心

原始碼解析

composer 自動載入總體來說就是兩部分 初始化載入自動載入。下面我們具體分析下 (在此之前請了解下 sql_aotoload() 基本用法)

1. 初始化

目的就是為了給 ClassLoader 檔案中的屬性進行初始化。初始化 psr0、psr4,以及類 map 陣列

# vim ./vendor/composer/ClassLoader.php :46

class ClassLoader
{
    // PSR-4
    // 該屬性初始化各個composer檔案中 PSR-4名稱空間長度
    private $prefixLengthsPsr4 = array();
    // 該屬性初始化各個composer檔案中 PSR-4名稱空間與路徑的對映關係
    private $prefixDirsPsr4 = array();
    // 該屬性初始化各個composer檔案中 PSR-4名稱空間為空與路徑的對映關係
    private $fallbackDirsPsr4 = array();

    // PSR-0
    // 該屬性初始化各個composer檔案中 PSR-0名稱空間與路徑對映的關係
    private $prefixesPsr0 = array();
    // 該屬性初始化各個composer檔案中 PSR-0名稱空間為空與路徑的對映關係
    private $fallbackDirsPsr0 = array();

    // Composer 自動載入器也會到 PHP 的 include_path (PHP.INI)中查詢
    private $useIncludePath = false;
    // 該屬性初始化類與檔案的位置的對映關係
    private $classMap = array();
    // 設定為true,Composer將只在classMap 中查詢檔案(適用於生產)
    private $classMapAuthoritative = false;
    // 儲存不存在的類,以便下次require時不在進行磁碟載入(提高效率 ClassLoader.php:379)
    private $missingClasses = array();
    // PHP APCU擴充,使用者快取
    private $apcuPrefix;
}

2. 載入自動載入(初始化已執行完成)

載入 loadClass
# vim ./vendor/composer/ClassLoader.php :306

/**
 * 註冊自動載入
 *
 * @parma $prepend bool 是否將loadClass 加入自動載入佇列之首
 */
public function register($prepend = false)
{
    spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
loadClass 原始碼分析

loadClass 核心方法

# vim ./vendor/composer/ClassLoader.php :325

/**
 * 自動載入查詢檔案,查詢到則包含
 *
 * @parma $class string 完全類名
 */
public function loadClass($class)
{
    if ($file = $this->findFile($class)) {
        includeFile($file);

        return true;
    }
}

findFile 查詢檔案

# vim ./vendor/composer/ClassLoader.php :341

/**
 * 通過完全類名查詢檔案
 * @param string $class 完全類名
 * @return string|false false為未查詢到檔案
 */
public function findFile($class) 
{
    // 先通過classMap 完美類名與路徑對應關係直接查詢
    if (isset($this->classMap[$class])) {
        return $this->classMap[$class];
    }

    // 如果使用者開發了classMapAuthoritative 或者 這個類之前未查詢到,則直接返回false
    if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
        return false;
    }

    // 如果開啟了 apcu 快取,則通過該擴充查詢
    if (null !== $this->apcuPrefix) {
        $file = apcu_fetch($this->apcuPrefix.$class, $hit);
        if ($hit) {
            return $file;
        }
    }

   // 通過PRS-0 與 PSR-4 規範查詢檔案
    $file = $this->findFileWithExtension($class, '.php');

    // Search for Hack files if we are running on HHVM(未查詢到通過,開啟了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;
}

符合 PSR-0 與 PSR-4 規範查詢檔案

# vim ./vendor/composer/ClassLoader.php :376

/**
 * 按照 PSR-0 與 PSR-4 查詢檔案
 * @param string $class 完全類名
 * @param string $ext 檔案字尾
 * @return string|false false為未查詢到檔案
 */
private function findFileWithExtension($class, $ext)
{
    /**
     * 1. 在 $this->prefixLengthsPsr4 查詢該類的名稱空間是否存在
     * 2. 在 $this->prefixDirsPsr4 查詢該名稱空間所對應的位置 
     * 3. 迴圈查詢返回的位置,按照 PSR-4 規範組裝檔案路徑,若檔案存在則返回退出 
     */
    // PSR-4 lookup
    // 按照 PSR-4 直接將類名轉化為檔案
    $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;
                    }
                }
            }
        }
    }

    /**
     * 迴圈查詢 $this->fallbackDirsPsr4 名稱空間為空所對應的目錄檔案
     */
    foreach ( $this->fallbackDirsPsr4  as $dir) {
        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
            return $file;
        }
    }

    /**
     * 1. 先按照 PRS-0 將名稱空間轉化成檔案路徑
     * 2. 在 $this->prefixesPsr0 查詢該類的名稱空間(首字母)是否存在
     * 3. 迴圈 $this->prefixesPsr0 中名稱空間所對應位置,並按照 PSR-0 檔案目錄與名稱空間對應規則組裝檔案路徑,若檔案存在則返回退出     
     */
    // 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;
                    }
                }
            }
        }
    }

    /**
     * 迴圈查詢 $this->fallbackDirsPsr0 名稱空間為空所對應的目錄檔案
     */
    foreach ($this->fallbackDirsPsr0 as $dir) {
        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
            return $file;
        }
    }

    // PSR-0 include paths.
    /**
     * 使用了 useIncludePath,並且能查詢到該檔案
     */
    if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
        return $file;
    }

    return false;
}

相關文章