Composer的Autoload源碼實現——啓動與初始化

前言


在開始以前,歡迎關注我本身的博客:www.leoyang90.cnphp

上一篇文章,咱們討論了 PHP 的自動加載原理、PHP 的命名空間、PHP 的 PSR0 與 PSR4 標準,有了這些知識,其實咱們就能夠按照 PSR4 標準寫出能夠自動加載的程序了。然而咱們爲何要本身寫呢?尤爲是有 Composer 這神同樣的包管理器的狀況下?css

Composer 自動加載概論


簡介

Composer 是 PHP 的一個依賴管理工具。它容許你申明項目所依賴的代碼庫,它會在你的項目中爲你安裝他們。詳細內容能夠查看 Composer 中文網
Composer Composer 將這樣爲你解決問題:html

  • 你有一個項目依賴於若干個庫。laravel

  • 其中一些庫依賴於其餘庫。git

  • 你聲明你所依賴的東西。github

  • Composer 會找出哪一個版本的包須要安裝,並安裝它們(將它們下載到你的項目中)。web

例如,你正在建立一個項目,你須要一個庫來作日誌記錄。你決定使用 monolog。爲了將它添加到你的項目中,你所須要作的就是建立一個 composer.json 文件,其中描述了項目的依賴關係。json

{
  "require": {
    "monolog/monolog": "1.2.*"
  }
}

而後咱們只要在項目裏面直接use MonologLogger便可,神奇吧!
簡單的說,Composer 幫助咱們下載好了符合 PSR0 或 PSR4 標準的第三方庫,並把文件放在相應位置;幫咱們寫了 _autoload() 函數,註冊到了 spl_register() 函數,當咱們想用第三方庫的時候直接使用命名空間便可。
  
那麼當咱們想要寫本身的命名空間的時候,該怎麼辦呢?很簡單,咱們只要按照 PSR4 標準命名咱們的命名空間,放置咱們的文件,而後在 composer 裏面寫好頂級域名與具體目錄的映射,就能夠享用 composer 的便利了。
固然若是有一個很是棒的框架,咱們會驚喜地發現,在 composer 裏面寫頂級域名映射這事咱們也不用作了,框架已經幫咱們寫好了頂級域名映射了,咱們只須要在框架裏面新建文件,在新建的文件中寫好命名空間,就能夠在任何地方 use 咱們的命名空間了。
下面咱們就以 laravel 框架爲例,講一講 composer 是如何實現 PSR0 和 PSR4 標準的自動加載功能。bootstrap

Composer 自動加載文件

首先,咱們先大體瞭解一下 Composer 自動加載所用到的源文件。數組

  1. autoload_real.php:自動加載功能的引導類。任務是 composer 加載類的初始化(頂級命名空間與文件路徑映射初始化)和註冊( spl_autoload_register() )。

  2. ClassLoader.php:composer 加載類。composer 自動加載功能的核心類。

  3. autoload_static.php:頂級命名空間初始化類,用於給核心類初始化頂級命名空間。

  4. autoload_classmap.php:自動加載的最簡單形式,有完整的命名空間和文件目錄的映射;

  5. autoload_files.php:用於加載全局函數的文件,存放各個全局函數所在的文件路徑名;

  6. autoload_namespaces.php:符合 PSR0 標準的自動加載文件,存放着頂級命名空間與文件的映射;

  7. autoload_psr4.php:符合 PSR4 標準的自動加載文件,存放着頂級命名空間與文件的映射;

啓動


laravel 框架的初始化是須要 composer 自動加載協助的,因此 laravel 的入口文件 index.php 第一句就是利用 composer 來實現自動加載功能。

require __DIR__.'/../bootstrap/autoload.php';

我們接着去看 bootstrap 目錄下的 autoload.php:

define('LARAVEL_START', microtime(true));

  require __DIR__.'/../vendor/autoload.php';

再去vendor目錄下的autoload.php:

require_once __DIR__ . '/composer' . '/autoload_real.php';

  return ComposerAutoloaderInit
  832ea71bfb9a4128da8660baedaac82e::getLoader();

爲何框架要在 bootstrap/autoload.php 轉一下?我的理解,laravel 這樣設計有利於支持或擴展任意有自動加載的第三方庫。
好了,咱們終於要看到了 Composer 真正要顯威的地方了。autoload_real 裏面就是一個自動加載功能的引導類,這個類不負責具體功能邏輯,只作了兩件事:初始化自動加載類、註冊自動加載類。
到 autoload_real 這個文件裏面去看,發現這個引導類的名字叫 ComposerAutoloaderInit832ea71bfb9a4128da8660baedaac82e,爲何要叫這麼古怪的名字呢?由於這是防止用戶自定義類名跟這個類重複衝突了,因此在類名上加了一個 hash 值。
其實還有一個緣由,那就是composer運行加載多個ComposerAutoloaderInit類。在實際狀況下可能會出現這樣的狀況:vendor/modelA/vendor/composer。也就是說第三方庫中也存在着一個composer,他有着本身所依賴的各類庫,也是經過composer來加載。這樣的話就會有兩個ComposerAutoloaderInit類,那麼就會觸發redeclare的錯誤。給ComposerAutoloaderInit加上一個hash,那麼就能夠實現多個class loader 的加載。

autoload_real 引導類


在 vendor 目錄下的 autoload.php 文件中咱們能夠看出,程序主要調用了引導類的靜態方法 getLoader(),咱們接着看看這個函數。

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

從上面能夠看出,我把自動加載引導類分爲5個部分。

第一部分——單例


第一部分很簡單,就是個最經典的單例模式,自動加載類只能有一個,屢次加載影響效率,可能會引發重複require同一個文件。

if (null !== self::$loader) {
  return self::$loader;
}

第二部分——構造 ClassLoader 核心類


第二部分 new 一個自動加載的核心類對象。

/***********************得到自動加載核心類對象********************/
  spl_autoload_register(array('ComposerAutoloaderInit
  832ea71bfb9a4128da8660baedaac82e', 'loadClassLoader'), true, true);

  self::$loader = $loader = new \Composer\Autoload\ClassLoader();

  spl_autoload_unregister(array('ComposerAutoloaderInit
               832ea71bfb9a4128da8660baedaac82e', 'loadClassLoader'));

loadClassLoader() 函數:

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

從程序裏面咱們能夠看出,composer 先向 PHP 自動加載機制註冊了一個函數,這個函數 require 了 ClassLoader 文件。成功 new 出該文件中核心類 ClassLoader() 後,又銷燬了該函數。
爲何不直接 require,而要這麼麻煩?緣由和ComposerAutoloaderInit加上hash同樣,若是直接require,那麼會形成ClassLoader類的重複定義。因此有人建議這樣:

if (!class_exists('Composer\Autoload\ClassLoader', false)) {
  require __DIR__ . '/ClassLoader.php';
}
static::\$loader = \$loader = new \\Composer\\Autoload\\ClassLoader();

其實這樣能夠更加直觀。可是class_exists有個缺點,那就是opcache緩存有個bug,class_exists即便爲真,程序仍然會進入if條件進行require,這樣仍然形成了重複定義的問題。
那爲何不跟引導類同樣用個 hash 呢?這樣就能夠屢次定義這個ClassLoader類了。緣由就是這個類是能夠複用的,框架容許用戶使用這個類,若是用hash用戶就徹底沒辦法用ClassLoader了。
因此最終的解決方案就是利用spl_autoload_register來加載,這樣只要ClassLoader只要被聲明過,spl_autoload_register就不會調用,也就不會require。
可見這簡單的幾行代碼其實內幕很深的。詳細可見
github 的相關 issue:Unable to run tests with phpunit and composer installed globally #1248
github 相關解決方案 PR : Allow loading of multiple composer autoloaders concurrently, fixes #1248 #1313

第三部分——初始化核心類對象


/***********************初始化自動加載核心類對象********************/
  $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);
  }
}

這一部分就是對自動加載類的初始化,主要是給自動加載核心類初始化頂級命名空間映射。初始化的方法有兩種:(1)使用 autoload_static 進行靜態初始化;(2)調用核心類接口初始化。

autoload_static 靜態初始化


靜態初始化只支持 PHP5.6 以上版本而且不支持 HHVM 虛擬機。爲何要單獨要求 php5.6 版本以上呢?緣由就是這種靜態加載加速機制是 opcache 緩存針對靜態數組優化的,只支持 php5.6 以上的版本。hhvm 是 php 另外一個虛擬機,固然沒有辦法支持 opcache 緩存。
github相關 PR: Speedup autoloading on PHP 5.6 & 7.0+ using static arrays
咱們深刻 autoload_static.php 這個文件發現這個文件定義了一個用於靜態初始化的類,名字叫 ComposerStaticInit832ea71bfb9a4128da8660baedaac82e,仍然爲了不衝突加了 hash 值和屢次複用。這個類很簡單:

class ComposerStaticInit832ea71bfb9a4128da8660baedaac82e{
    public static $files = array(...);
    public static $prefixLengthsPsr4 = array(...);
    public static $prefixDirsPsr4 = array(...);
    public static $prefixesPsr0 = array(...);
    public static $classMap = array (...);

    public static function getInitializer(ClassLoader $loader)
    {
        return \Closure::bind(function () use ($loader) {
            $loader->prefixLengthsPsr4 =   ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::$prefixLengthsPsr4;
            $loader->prefixDirsPsr4 = ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::$prefixDirsPsr4;
            $loader->prefixesPsr0 = ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::$prefixesPsr0;
            $loader->classMap = ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::$classMap;

        }, null, ClassLoader::class);
    }
}

這個靜態初始化類的核心就是 getInitializer() 函數,它將本身類中的頂級命名空間映射給了 ClassLoader 類。值得注意的是這個函數返回的是一個匿名函數,爲何呢?緣由就是 ClassLoader 類中的 prefixLengthsPsr四、prefixDirsPsr4 等等都是 private的。。。普通的函數沒辦法給類的 private 成員變量賦值。利用匿名函數的綁定功能就能夠將把匿名函數轉爲 ClassLoader 類的成員函數。
關於匿名函數的 綁定功能
接下來就是頂級命名空間初始化的關鍵了。

最簡單的classMap:

public static $classMap = array (
    'App\\Console\\Kernel' => __DIR__ . '/../..' . '/app/Console/Kernel.php',
    'App\\Exceptions\\Handler' => __DIR__ . '/../..' . '/app/Exceptions/Handler.php',
    'App\\Http\\Controllers\\Auth\\ForgotPasswordController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/ForgotPasswordController.php',
    'App\\Http\\Controllers\\Auth\\LoginController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/LoginController.php',
    'App\\Http\\Controllers\\Auth\\RegisterController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/RegisterController.php',
...
)

簡單吧,直接命名空間全名與目錄的映射,沒有頂級命名空間。。。簡單粗暴,也致使這個數組至關的大。

PSR0頂級命名空間映射:

public static $prefixesPsr0 = array (
  'P' =>
  array (
    'Prophecy\\' =>
    array (
      0 => __DIR__ . '/..' . '/phpspec/prophecy/src',
    ),
    'Parsedown' =>
    array (
      0 => __DIR__ . '/..' . '/erusev/parsedown',
    ),
  ),
  'M' =>
  array (
    'Mockery' =>
    array (
      0 => __DIR__ . '/..' . '/mockery/mockery/library',
    ),
  ),
  'J' =>
  array (
    'JakubOnderka\\PhpConsoleHighlighter' =>
    array (
      0 => __DIR__ . '/..' . '/jakub-onderka/php-console-highlighter/src',
    ),
    'JakubOnderka\\PhpConsoleColor' =>
    array (
      0 => __DIR__ . '/..' . '/jakub-onderka/php-console-color/src',
    ),
  ),
  'D' =>
  array (
    'Doctrine\\Common\\Inflector\\' =>
    array (
      0 => __DIR__ . '/..' . '/doctrine/inflector/lib',
    ),
  ),
);

爲了快速找到頂級命名空間,咱們這裏使用命名空間第一個字母做爲前綴索引。這個映射的用法比較明顯,假如咱們有 Parsedown/example 這樣的命名空間,首先經過首字母 P,找到

'P' =>
  array (
    'Prophecy\\' =>
    array (
      0 => __DIR__ . '/..' . '/phpspec/prophecy/src',
    ),
    'Parsedown' =>
    array (
      0 => __DIR__ . '/..' . '/erusev/parsedown',
    ),
  )

這個數組,而後咱們就會遍歷這個數組來和 Parsedown/example 比較,發現第一個 Prophecy 不符合,第二個 Parsedown 符合,而後獲得了映射目錄:(映射目錄可能不止一個)

array (
  0 => __DIR__ . '/..' . '/erusev/parsedown',
)

咱們會接着遍歷這個數組,嘗試 _DIR_ .'/..' . '/erusev/parsedown/Parsedown/example.php 是否存在,若是不存在接着遍歷數組(這個例子數組只有一個元素),若是數組遍歷完都沒有,就會加載失敗。

PSR4標準頂級命名空間映射數組:

public static $prefixLengthsPsr4 = array(
  'p' =>
  array (
    'phpDocumentor\\Reflection\\' => 25,
  ),
  'S' =>
  array (
    'Symfony\\Polyfill\\Mbstring\\' => 26,
    'Symfony\\Component\\Yaml\\' => 23,
    'Symfony\\Component\\VarDumper\\' => 28,
    ...
  ),
  ...
);

public static $prefixDirsPsr4 = array (
  'phpDocumentor\\Reflection\\' =>
  array (
    0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',
    1 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',
    2 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src',
  ),
  'Symfony\\Polyfill\\Mbstring\\' =>
  array (
    0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
  ),
  'Symfony\\Component\\Yaml\\' =>
  array (
    0 => __DIR__ . '/..' . '/symfony/yaml',
  ),
  ...
)

PSR4 標準頂級命名空間映射用了兩個數組,第一個和 PSR0 同樣用命名空間第一個字母做爲前綴索引,而後是頂級命名空間,可是最終並非文件路徑,而是頂級命名空間的長度。爲何呢?由於前一篇 文章 咱們說過,PSR4 標準的文件目錄更加靈活,更加簡潔。PSR0 中頂級命名空間目錄直接加到命名空間前面就能夠獲得路徑 (Parsedown/example => _DIR_ .'/..' . '/erusev/parsedown/Parsedown/example.php),而 PSR4 標準倒是用頂級命名空間目錄替換頂級命名空間(Parsedown/example => _DIR_ .'/..' . '/erusev/parsedown/example.php),因此得到頂級命名空間的長度很重要。
具體的用法:假如咱們找 Symfony\Polyfill\Mbstring\example 這個命名空間,和 PSR0 同樣經過前綴索引和字符串匹配咱們獲得了

'Symfony\\Polyfill\\Mbstring\\' => 26,

這條記錄,鍵是頂級命名空間,值是命名空間的長度。拿到頂級命名空間後去 $prefixDirsPsr4 數組獲取它的映射目錄數組:(注意映射目錄可能不止一條)

'Symfony\\Polyfill\\Mbstring\\' =>
array (
  0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
)

而後咱們就能夠將命名空間 Symfony\Polyfill\Mbstring\example 前26個字符替換成目錄 _DIR_ . '/..' . '/symfony/polyfill-mbstring,咱們就獲得了 _DIR_ . '/..' . '/symfony/polyfill-mbstring/example.php,先驗證磁盤上這個文件是否存在,若是不存在接着遍歷。若是遍歷後沒有找到,則加載失敗。
  
自動加載核心類 ClassLoader 的靜態初始化完成!!!

ClassLoader 接口初始化


若是PHP版本低於5.6或者使用HHVM虛擬機環境,那麼就要使用核心類的接口進行初始化。

//PSR0標準
  $map = require __DIR__ . '/autoload_namespaces.php';
  foreach ($map as $namespace => $path) {
      $loader->set($namespace, $path);
  }

  //PSR4標準
  $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);
  }

PSR0 標準

autoload_namespaces:

return array(
  'Prophecy\\' => array($vendorDir . '/phpspec/prophecy/src'),
  'Parsedown' => array($vendorDir . '/erusev/parsedown'),
  'Mockery' => array($vendorDir . '/mockery/mockery/library'),
  'JakubOnderka\\PhpConsoleHighlighter' => array($vendorDir . '/jakub-onderka/php-console-highlighter/src'),
  'JakubOnderka\\PhpConsoleColor' => array($vendorDir . '/jakub-onderka/php-console-color/src'),
  'Doctrine\\Common\\Inflector\\' => array($vendorDir . '/doctrine/inflector/lib'),
);

PSR0 標準的初始化接口:

public function set($prefix, $paths)
{
  if (!$prefix) {
    $this->fallbackDirsPsr0 = (array) $paths;
  } else {
    $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
  }
}

很簡單,PSR0 標準取出命名空間的第一個字母做爲索引,一個索引對應多個頂級命名空間,一個頂級命名空間對應多個目錄路徑,具體形式能夠查看上面咱們講的 autoload_static 的 $prefixesPsr0。若是沒有頂級命名空間,就只存儲一個路徑名,以便在後面嘗試加載。

PSR4標準

autoload_psr4

return array(
  'XdgBaseDir\\' => array($vendorDir . '/dnoegel/php-xdg-base-dir/src'),
  'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'),
  'TijsVerkoyen\\CssToInlineStyles\\' => array($vendorDir . '/tijsverkoyen/css-to-inline-styles/src'),
  'Tests\\' => array($baseDir . '/tests'),
  'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
  ...
)

PSR4 標準的初始化接口:

public function setPsr4($prefix, $paths)
{
  if (!$prefix) {
    $this->fallbackDirsPsr4 = (array) $paths;
  } else {
    $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;
  }
}

PSR4 初始化接口也很簡單。若是沒有頂級命名空間,就直接保存目錄。若是有命名空間的話,要保證頂級命名空間最後是\,而後分別保存(前綴=》頂級命名空間,頂級命名空間=》頂級命名空間長度),(頂級命名空間=》目錄)這兩個映射數組。具體形式能夠查看上面咱們講的 autoload_static的prefixLengthsPsr四、 $prefixDirsPsr4。

傻瓜式命名空間映射

autoload_classmap:

public static $classMap = array (
  'App\\Console\\Kernel' => __DIR__ . '/../..' . '/app/Console/Kernel.php',
  'App\\Exceptions\\Handler' => __DIR__ . '/../..' . '/app/Exceptions/Handler.php',
  ...
)

addClassMap:

public function addClassMap(array $classMap)
{
  if ($this->classMap) {
    $this->classMap = array_merge($this->classMap, $classMap);
  } else {
    $this->classMap = $classMap;
  }
}

這個最簡單,就是整個命名空間與目錄之間的映射。

結語

其實我很想接着寫下下去,可是這樣會形成篇幅過長,因此我就把自動加載的註冊和運行放到下一篇文章了。咱們回顧一下,這篇文章主要講了:(1)框架如何啓動 composer 自動加載;(2)composer 自動加載分爲5部分;
其實說是5部分,真正重要的就兩部分——初始化與註冊。初始化負責頂層命名空間的目錄映射,註冊負責實現頂層如下的命名空間映射規則。

Written with StackEdit.

相關文章
相關標籤/搜索