Composer的Autoload源碼實現——註冊與運行

前言


在開始以前,歡迎關注我本身的博客:www.leoyang90.cn
上一篇 文章咱們講到了 Composer 自動加載功能的啓動與初始化,通過啓動與初始化,自動加載核心類對象已經得到了頂級命名空間與相應目錄的映射,換句話說,若是有命名空間 'App\Console\Kernel,咱們已經知道了 App\ 對應的目錄,接下來咱們就要解決下面的就是 \Console\Kernel這一段。php

註冊


咱們先回顧一下自動加載引導類:laravel

public static function getLoader()
{
        /***************************經典單例模式********************/
        if (null !== self::$loader) {
            return self::$loader;
        }
        
        /***********************得到自動加載核心類對象********************/
        spl_autoload_register(array('ComposerAutoloaderInit
        832ea71bfb9a4128da8660baedaac82e', 'loadClassLoader'), true, true);
        
        self::$loader = $loader = new \Composer\Autoload\ClassLoader();
        
        spl_autoload_unregister(array('ComposerAutoloaderInit
        832ea71bfb9a4128da8660baedaac82e', 'loadClassLoader'));

        /***********************初始化自動加載核心類對象********************/
        $useStaticLoader = PHP_VERSION_ID >= 50600 && 
        !defined('HHVM_VERSION');
        
        if ($useStaticLoader) {
            require_once __DIR__ . '/autoload_static.php';

            call_user_func(\Composer\Autoload\ComposerStaticInit
            832ea71bfb9a4128da8660baedaac82e::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\ComposerStaticInit
            832ea71bfb9a4128da8660baedaac82e::$files;
        } else {
            $includeFiles = require __DIR__ . '/autoload_files.php';
        }
        
        foreach ($includeFiles as $fileIdentifier => $file) {
            composerRequire
            832ea71bfb9a4128da8660baedaac82e($fileIdentifier, $file);
        }

        return $loader;
    }

如今咱們開始引導類的第四部分:註冊自動加載核心類對象。咱們來看看核心類的 register() 函數:bootstrap

public function register($prepend = false)
{
    spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}

簡單到爆炸啊!一行代碼實現自動加載有木有!其實奧祕都在自動加載核心類 ClassLoader 的 loadClass() 函數上,這個函數負責按照 PSR 標準將頂層命名空間如下的內容轉爲對應的目錄,也就是上面所說的將 'App\Console\Kernel中'Console\Kernel 這一段轉爲目錄,至於怎麼轉的咱們在下面「Composer 自動加載源碼分析——運行」講。核心類 ClassLoader 將 loadClass() 函數註冊到 PHP SPL 中的spl_autoload_register() 裏面去,這個函數的前因後果咱們以前 文章 講過。這樣,每當 PHP 遇到一個不認識的命名空間的時候,PHP 會自動調用註冊到 spl_autoload_register 裏面的函數堆棧,運行其中的每一個函數,直到找到命名空間對應的文件。數組

全局函數的自動加載

Composer 不止能夠自動加載命名空間,還能夠加載全局函數。怎麼實現的呢?很簡單,把全局函數寫到特定的文件裏面去,在程序運行前挨個 require 就好了。這個就是 composer 自動加載的第五步,加載全局函數。app

if ($useStaticLoader) {
    $includeFiles = Composer\Autoload\ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::$files;
} else {
    $includeFiles = require __DIR__ . '/autoload_files.php';
}

foreach ($includeFiles as $fileIdentifier => $file) {
    composerRequire832ea71bfb9a4128da8660baedaac82e($fileIdentifier, $file);
}

跟核心類的初始化同樣,全局函數自動加載也分爲兩種:靜態初始化和普通初始化,靜態加載只支持 PHP5.6 以上而且不支持 HHVM。composer

靜態初始化:

ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::$files:框架

public static $files = array (
      '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
      '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
      ...
    );

看到這裏咱們可能又要有疑問了,爲何不直接放文件路徑名,還要一個 hash 幹什麼呢?這個咱們一下子講,咱們這裏先了解一下這個數組的結構。函數

普通初始化

autoload_files:源碼分析

$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
    
return array(
    '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
    '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
     ....
);

其實跟靜態初始化區別不大。ui

加載全局函數

class ComposerAutoloaderInit832ea71bfb9a4128da8660baedaac82e{
    public static function getLoader(){
        ...
        foreach ($includeFiles as $fileIdentifier => $file) {
            composerRequire832ea71bfb9a4128da8660baedaac82e($fileIdentifier, $file);
        }
        ...
    }
}

function composerRequire832ea71bfb9a4128da8660baedaac82e($fileIdentifier, $file)
{
      if (empty(\$GLOBALS['__composer_autoload_files'][\$fileIdentifier])) {
          require $file;

          $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
      }
}

這一段頗有講究,
第一個問題:爲何自動加載引導類的 getLoader() 函數不直接 require includeFiles 裏面的每一個文件名,而要用類外面的函數composerRequire832ea71bfb9a4128da8660baedaac82e0?(順便說下這個函數名 hash 仍然爲了不和用戶定義函數衝突)由於怕有人在全局函數所在的文件寫 this 或者self。
假如 includeFiles 有個 app/helper.php 文件,這個 helper.php 文件的函數外有一行代碼:this->foo(),若是引導類在 getLoader() 函數直接 require(file),那麼引導類就會運行這句代碼,調用本身的 foo() 函數,這顯然是錯的。事實上 helper.php 就不該該出現 this 或 self 這樣的代碼,這樣寫通常都是用戶寫錯了的,一旦這樣的事情發生,第一種狀況:引導類剛好有 foo() 函數,那麼就會莫名其妙執行了引導類的 foo();第二種狀況:引導類沒有 foo() 函數,可是卻甩出來引導類沒有 foo() 方法這樣的錯誤提示,用戶不知道本身哪裏錯了。把 require 語句放到引導類的外面,遇到 this 或者 self,程序就會告訴用戶根本沒有類,this 或 self 無效,錯誤信息更加明朗。

第二個問題,爲何要用 hash 做爲 fileIdentifier,上面的代碼明顯能夠看出來這個變量是用來控制全局函數只被 require 一次的,那爲何不用 require_once 呢?事實上require_once 比 require 效率低不少,使用全局變量 GLOBALS 這樣控制加載會更快。

可是其實也帶來了一些問題,若是存在兩個自動加載,並且全局函數的相對路徑不一致,很容易形成 hash 不相同,可是文件相同的狀況,致使重複定義函數。因此在使用 composer 的時候最好要統一自動加載和依賴機制,最好不要多重自動加載。

運行

咱們終於來到了核心的核心——composer 自動加載的真相,命名空間如何經過 composer 轉爲對應目錄文件的奧祕就在這一章。
前面說過,ClassLoader的register() 函數將 loadClass() 函數註冊到 PHP 的 SPL 函數堆棧中,每當 PHP 遇到不認識的命名空間時就會調用函數堆棧的每一個函數,直到加載命名空間成功。因此 loadClass() 函數就是自動加載的關鍵了。
loadClass():

public function loadClass($class)
{
if ($file = $this->findFile($class)) {
    includeFile($file);

    return true;
}
}

public function findFile($class)
{
// work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
if ('\\' == $class[0]) {
    $class = substr($class, 1);
}

// class map lookup
if (isset($this->classMap[$class])) {
    return $this->classMap[$class];
}
if ($this->classMapAuthoritative) {
    return false;
}

$file = $this->findFileWithExtension($class, '.php');

// Search for Hack files if we are running on HHVM
if ($file === null && defined('HHVM_VERSION')) {
    $file = $this->findFileWithExtension($class, '.hh');
}

if ($file === null) {
    // Remember that this class does not exist.
    return $this->classMap[$class] = false;
}

return $file;
}

咱們看到 loadClass(),主要調用 findFile() 函數。findFile() 在解析命名空間的時候主要分爲兩部分:classMap 和 findFileWithExtension() 函數。classMap 很簡單,直接看命名空間是否在映射數組中便可。麻煩的是 findFileWithExtension() 函數,這個函數包含了 PSR0 和 PSR4 標準的實現。還有個值得咱們注意的是查找路徑成功後 includeFile() 仍然類外面的函數,並非 ClassLoader 的成員函數,原理跟上面同樣,防止有用戶寫 $this 或 self。還有就是若是命名空間是以 \ 開頭的,要去掉 \ 而後再匹配。
findFileWithExtension:

private function findFileWithExtension($class, $ext)
{
      // PSR-4 lookup
      $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;

      $first = $class[0];
      if (isset($this->prefixLengthsPsr4[$first])) {
          foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) {
              if (0 === strpos($class, $prefix)) {
                  foreach ($this->prefixDirsPsr4[$prefix] as $dir) {
                      if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
                          return $file;
                      }
                  }
              }
          }
      }

      // PSR-4 fallback dirs
      foreach ($this->fallbackDirsPsr4 as $dir) {
          if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
              return $file;
          }
      }

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

      // PSR-0 fallback dirs
      foreach ($this->fallbackDirsPsr0 as $dir) {
          if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
              return $file;
          }
      }

      // PSR-0 include paths.
      if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
          return $file;
      }
}

下面咱們經過舉例來講下上面代碼的流程:
若是咱們在代碼中寫下 'phpDocumentor\Reflection\example',PHP 會經過 SPL 調用 loadClass->findFile->findFileWithExtension。首先默認用 php 做爲文件後綴名調用 findFileWithExtension 函數裏,利用 PSR4 標準嘗試解析目錄文件,若是文件不存在則繼續用 PSR0 標準解析,若是解析出來的目錄文件仍然不存在,可是環境是 HHVM 虛擬機,繼續用後綴名爲 hh 再次調用 findFileWithExtension 函數,若是不存在,說明此命名空間沒法加載,放到 classMap 中設爲 false,以便之後更快地加載。
對於 phpDocumentor\Reflection\example,當嘗試利用 PSR4 標準映射目錄時,步驟以下:

PSR4 標準加載

  • 將 \ 轉爲文件分隔符 /,加上後綴 php 或 hh,獲得 $logicalPathPsr4 即 phpDocumentor//Reflection//example.php(hh);

  • 利用命名空間第一個字母 p 做爲前綴索引搜索 prefixLengthsPsr4 數組,查到下面這個數組:

p' => 
    array (
       'phpDocumentor\\Reflection\\' => 25,
       'phpDocumentor\\Fake\\' => 19,
    )
  • 遍歷這個數組,獲得兩個頂層命名空間 phpDocumentor\Reflection\ 和 phpDocumentor\Fake\

  • 用這兩個頂層命名空間與 phpDocumentor\Reflection\example_e 相比較,能夠獲得 phpDocumentor\Reflection\ 這個頂層命名空間

  • 在 prefixLengthsPsr4 映射數組中獲得 phpDocumentor\Reflection\ 長度爲25。

  • 在 prefixDirsPsr4 映射數組中獲得 phpDocumentor\Reflection\ 的目錄映射爲:

'phpDocumentor\\Reflection\\' => 
    array (
        0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',
        1 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',
        2 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src',
),
  • 遍歷這個映射數組,獲得三個目錄映射;

  • 查看 「目錄+文件分隔符 //+substr($logicalPathPsr4, $length)」 文件是否存在,存在即返回。這裏就是 '__DIR__/../phpdocumentor/reflection-common/src + /+ substr(phpDocumentor/Reflection/example_e.php(hh),25)'

  • 若是失敗,則利用 fallbackDirsPsr4 數組裏面的目錄繼續判斷是否存在文件,具體方法是「目錄+文件分隔符//+$logicalPathPsr4」

PSR0 標準加載

若是 PSR4 標準加載失敗,則要進行 PSR0 標準加載:

  • 找到 phpDocumentor\Reflection\example_e 最後「\」的位置,將其後面文件名中’‘_’‘字符轉爲文件分隔符「/」,獲得 logicalPathPsr0 即 phpDocumentor/Reflection/example/e.php(hh)
    利用命名空間第一個字母 p 做爲前綴索引搜索 prefixLengthsPsr4 數組,查到下面這個數組:

'P' => 
        array (
            'Prophecy\\' => 
            array (
                0 => __DIR__ . '/..' . '/phpspec/prophecy/src',
            ),
            'phpDocumentor' => 
            array (
                0 => __DIR__ . '/..' . '/erusev/parsedown',
            ),
        ),
  • 遍歷這個數組,獲得兩個頂層命名空間phpDocumentor和Prophecy

  • 用這兩個頂層命名空間與 phpDocumentor\Reflection\example_e 相比較,能夠獲得 phpDocumentor 這個頂層命名空間

  • 在映射數組中獲得 phpDocumentor 目錄映射爲 '_DIR_ . '/..' . '/erusev/parsedown'

  • 查看 「目錄+文件分隔符//+logicalPathPsr0」文件是否存在,存在即返回。這裏就是
    「_DIR_ . '/..' . '/erusev/parsedown + //+ phpDocumentor//Reflection//example/e.php(hh)」

  • 若是失敗,則利用 fallbackDirsPsr0 數組裏面的目錄繼續判斷是否存在文件,具體方法是「目錄+文件分隔符//+logicalPathPsr0」

  • 若是仍然找不到,則利用 stream_resolve_include_path(),在當前 include 目錄尋找該文件,若是找到返回絕對路徑。

結語

通過三篇文章,終於寫完了 PHP Composer 自動加載的原理與實現,結下來咱們開始講解 laravel 框架下的門面 Facade,這個門面功能和自動加載有着一些聯繫.

相關文章
相關標籤/搜索