Composer 的 Autoload 原始碼實現——啟動與初始化

leoyang發表於2017-05-11

  在開始之前,歡迎關注我自己的部落格:www.learnku.com/blog/leoyang
本文 GitBook 地址: https://www.gitbook.com/book/leoyang90/lar...

  上一篇文章,我們討論了PHP的自動載入原理、PHP的名稱空間、PHP的PSR0與PSR4標準,有了這些知識,其實我們就可以按照PSR4標準寫出可以自動載入的程式了。然而我們為什麼要自己寫呢?尤其是有Composer這神一樣的包管理器的情況下?


簡介

  Composer 是 PHP 的一個依賴管理工具。它允許你申明專案所依賴的程式碼庫,它會在你的專案中為你安裝他們。詳細內容可以檢視Composer 中文網
  Composer Composer 將這樣為你解決問題:

  • 你有一個專案依賴於若干個庫。
  • 其中一些庫依賴於其他庫。
  • 你宣告你所依賴的東西。
  • Composer 會找出哪個版本的包需要安裝,並安裝它們(將它們下載到你的專案中)。

   例如,你正在建立一個專案,你需要一個庫來做日誌記錄。你決定使用 monolog。為了將它新增到你的專案中,你所需要做的就是建立一個 composer.json 檔案,其中描述了專案的依賴關係。

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

  然後我們只要在專案裡面直接use Monolog\Logger即可,神奇吧!
   簡單的說,Composer幫助我們下載好了符合PSR0或PSR4標準的第三方庫,並把檔案放在相應位置;幫我們寫了_autoload()函式,註冊到了spl_register()函式,當我們想用第三方庫的時候直接使用名稱空間即可。
  
  那麼當我們想要寫自己的名稱空間的時候,該怎麼辦呢?很簡單,我們只要按照PSR4標準命名我們的名稱空間,放置我們的檔案,然後在composer裡面寫好頂級域名與具體目錄的對映,就可以享用composer的便利了。
   當然如果有一個非常棒的框架,我們會驚喜地發現,在composer裡面寫頂級域名對映這事我們也不用做了,框架已經幫我們寫好了頂級域名對映了,我們只需要在框架裡面新建檔案,在新建的檔案中寫好名稱空間,就可以在任何地方use我們的名稱空間了。
   下面我們就以laravel框架為例,講一講composer是如何實現PSR0和PSR4標準的自動載入功能。

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值。其實還有一個做法我們更加熟悉,那就是不直接定義類名,而是定義一個名稱空間。這裡為什麼不定義一個名稱空間呢?個人理解:名稱空間一般都是為了複用,而這個類只需要執行一次即可,以後也不會用得到,用hash值更加合適。


  在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個部分。

第一部分——單例


第一部分很簡單,就是個最經典的單例模式,自動載入類只能有一個。

  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,而要這麼麻煩?原因就是怕有的使用者也定義了個\Composer\Autoload\ClassLoader名稱空間,導致自動載入錯誤檔案。那為什麼不跟引導類一樣用個hash呢?因為這個類是可以複用的,框架允許使用者使用這個類。

第三部分——初始化核心類物件


  /***********************初始化自動載入核心類物件********************/
  $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虛擬機器。我們深入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類中的prefixLengthsPsr4、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的prefixLengthsPsr4、 $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.

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章