深入解析 composer 的自動加載原理

深入解析 composer 的自動加載原理

前言

PHP 自5.3的版本之後,已經重煥新生,命名空間、性狀(trait)、閉包、接口、PSR 規範、以及 composer 的出現已經讓 PHP 變成了一門現代化的腳本語言。PHP 的生態系統也一直在演進,而 composer 的出現更是徹底的改變了以往構建 PHP 應用的方式,我們可以根據 PHP 的應用需求混合搭配最合適的 PHP 組件。當然這也得益於 PSR 規範的提出。


大綱

  • PHP 自動加載功能
  • PSR 規範
  • comoposer 的自動加載過程
  • composer 源碼分析

一、PHP 自動加載功能

PHP 自動加載功能的由來

在 PHP 開發過程中,如果希望從外部引入一個 Class ,通常會使用 includerequire 方法,去把定義這個 Class 的文件包含進來。這個在小規模開發的時候,沒什麼大問題。但在大型的開發項目中,使用這種方式會帶來一些隱含的問題:如果一個 PHP 文件需要使用很多其它類,那麼就需要很多的 require/include 語句,這樣有可能會 造成遺漏 或者 包含進不必要的類文件。如果大量的文件都需要使用其它的類,那麼要保證每個文件都包含正確的類文件肯定是一個噩夢, 況且 require或 incloud 的性能代價很大。

PHP5 爲這個問題提供了一個解決方案,這就是 類的自動加載(autoload)機制autoload機制 可以使得 PHP 程序有可能在使用類時才自動包含類文件,而不是一開始就將所有的類文件include進來,這種機制也稱爲 Lazy loading (惰性加載)

  • 總結起來,自動加載功能帶來了幾處優點:

    1. 使用類之前無需 include / require
    2. 使用類的時候纔會 include / require 文件,實現了 lazy loading ,避免了 include / require 多餘文件。
    3. 無需考慮引入 類的實際磁盤地址 ,實現了邏輯和實體文件的分離。

PHP 自動加載函數 __autoload()

  • 從 PHP5 開始,當我們在使用一個類時,如果發現這個類沒有加載,就會自動運行 __autoload() 函數,這個函數是我們在程序中自定義的,在這個函數中我們可以加載需要使用的類。下面是個簡單的示例:

    <?php
    
    function __autoload($classname) {
            require_once ($classname . ".class.php");
    }
  • 在我們這個簡單的例子中,我們直接將類名加上擴展名 .class.php 構成了類文件名,然後使用 require_once 將其加載。

    從這個例子中,我們可以看出 __autoload 至少要做三件事情:

    1. 根據類名確定類文件名;
    2. 確定類文件所在的磁盤路徑;
    3. 將類從磁盤文件中加載到系統中。
  • 第三步最簡單,只需要使用 include / require 即可。要實現第一步,第二步的功能,必須在開發時約定類名與磁盤文件的映射方法,只有這樣我們才能根據類名找到它對應的磁盤文件。
  • 當有大量的類文件要包含的時候,我們只要確定相應的規則,然後在 __autoload() 函數中,將類名與實際的磁盤文件對應起來,就可以實現 lazy loading 的效果
  • 如果想詳細的瞭解關於 autoload 自動加載的過程,可以查看手冊資料:PHP autoload函數說明

__autoload() 函數存在的問題

  • 如果在一個系統的實現中,如果需要使用很多其它的類庫,這些類庫可能是由不同的開發人員編寫的, 其類名與實際的磁盤文件的映射規則不盡相同。這時如果要實現類庫文件的自動加載,就必須 在 __autoload() 函數中將所有的映射規則全部實現,這樣的話 __autoload() 函數有可能會非常複雜,甚至無法實現。最後可能會導致 __autoload() 函數十分臃腫,這時即便能夠實現,也會給將來的維護和系統效率帶來很大的負面影響。
  • 那麼問題出現在哪裏呢?問題出現在 __autoload() 是全局函數只能定義一次 ,不夠靈活,所以所有的類名與文件名對應的邏輯規則都要在一個函數裏面實現,造成這個函數的臃腫。那麼如何來解決這個問題呢?答案就是使用一個 __autoload調用堆棧 ,不同的映射關係寫到不同的 __autoload函數 中去,然後統一註冊統一管理,這個就是 PHP5 引入的 SPL Autoload

SPL Autoload

  • SPL是 Standard PHP Library(標準PHP庫)的縮寫。它是 PHP5 引入的一個擴展標準庫,包括 spl autoload 相關的函數以及各種數據結構和迭代器的接口或類。spl autoload 相關的函數具體可見 php中spl_autoload
<?php

// __autoload 函數
//
// function __autoload($class) {
//     include 'classes/' . $class . '.class.php';
// }


function my_autoloader($class) {
    include 'classes/' . $class . '.class.php';
}

spl_autoload_register('my_autoloader');


// 定義的 autoload 函數在 class 裏

// 靜態方法
class MyClass {
  public static function autoload($className) {
    // ...
  }
}

spl_autoload_register(array('MyClass', 'autoload'));

// 非靜態方法
class MyClass {
  public function autoload($className) {
    // ...
  }
}

$instance = new MyClass();
spl_autoload_register(array($instance, 'autoload'));

spl_autoload_register() 就是我們上面所說的__autoload調用堆棧,我們可以向這個函數註冊多個我們自己的 autoload() 函數,當 PHP 找不到類名時,PHP就會調用這個堆棧,然後去調用自定義的 autoload() 函數,實現自動加載功能。如果我們不向這個函數輸入任何參數,那麼就會默認註冊 spl_autoload() 函數。


二、PSR 規範

與自動加載相關的規範是 PSR4,在說 PSR4 之前先介紹一下 PSR 標準。PSR 標準的發明和推出組織是:PHP-FIG,它的網站是:www.php-fig.org。由幾位開源框架的開發者成立於 2009 年,從那開始也選取了很多其他成員進來,雖然不是 “官方” 組織,但也代表了社區中不小的一塊。組織的目的在於:以最低程度的限制,來統一各個項目的編碼規範,避免各家自行發展的風格阻礙了程序員開發的困擾,於是大夥發明和總結了 PSR,PSR 是 PHP Standards Recommendation 的縮寫,截止到目前爲止,總共有 14 套 PSR 規範,其中有 7 套PSR規範已通過表決並推出使用,分別是:

PSR-0 自動加載標準(已廢棄,一些舊的第三方庫還有在使用)

PSR-1 基礎編碼標準

PSR-2 編碼風格嚮導

PSR-3 日誌接口

PSR-4 自動加載的增強版,替換掉了 PSR-0

PSR-6 緩存接口規範

PSR-7 HTTP 消息接口規範

具體詳細的規範標準可以查看PHP 標準規範

PSR4 標準

2013 年底,PHP-FIG 推出了第 5 個規範——PSR-4。

PSR-4 規範瞭如何指定文件路徑從而自動加載類定義,同時規範了自動加載文件的位置。

1)一個完整的類名需具有以下結構:

\<命名空間>\<子命名空間>\<類名>

  • 完整的類名必須要有一個頂級命名空間,被稱爲 "vendor namespace";
  • 完整的類名可以有一個或多個子命名空間;
  • 完整的類名必須有一個最終的類名;
  • 完整的類名中任意一部分中的下滑線都是沒有特殊含義的;
  • 完整的類名可以由任意大小寫字母組成;
  • 所有類名都必須是大小寫敏感的。

2)根據完整的類名載入相應的文件

  • 完整的類名中,去掉最前面的命名空間分隔符,前面連續的一個或多個命名空間和子命名空間,作爲「命名空間前綴」,其必須與至少一個「文件基目錄」相對應;
  • 緊接命名空間前綴後的子命名空間 必須 與相應的「文件基目錄」相匹配,其中的命名空間分隔符將作爲目錄分隔符。
  • 末尾的類名必須與對應的以 .php 爲後綴的文件同名。
  • 自動加載器(autoloader)的實現一定不可拋出異常、一定不可觸發任一級別的錯誤信息以及不應該有返回值。

3) 例子

PSR-4風格

類名:ZendAbc
命名空間前綴:Zend
文件基目錄:/usr/includes/Zend/
文件路徑:/usr/includes/Zend/Abc.php
類名:SymfonyCoreRequest
命名空間前綴:SymfonyCore
文件基目錄:./vendor/Symfony/Core/
文件路徑:./vendor/Symfony/Core/Request.php

目錄結構

-vendor/
| -vendor_name/
| | -package_name/
| | | -src/
| | | | -ClassName.php       # Vendor_Name\Package_Name\ClassName
| | | -tests/
| | | | -ClassNameTest.php   # Vendor_Name\Package_Name\ClassNameTest

Composer自動加載過程

Composer 做了哪些事情

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

例如,你正在創建一個項目,需要做一些單元測試。你決定使用 phpunit 。爲了將它添加到你的項目中,你所需要做的就是在 composer.json 文件裏描述項目的依賴關係。

 {
   "require": {
     "phpunit/phpunit":"~6.0",
   }
 }

然後在 composer require 之後我們只要在項目裏面直接 use phpunit 的類即可使用。

執行 composer require 時發生了什麼

  • composer 會找到符合 PR4 規範的第三方庫的源
  • 將其加載到 vendor 目錄下
  • 初始化頂級域名的映射並寫入到指定的文件裏

(如:'PHPUnit\\Framework\\Assert' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Assert.php'

  • 寫好一個 autoload 函數,並且註冊到 spl_autoload_register()裏

題外話:現在很多框架都已經幫我們寫好了頂級域名映射了,我們只需要在框架裏面新建文件,在新建的文件中寫好命名空間,就可以在任何地方 use 我們的命名空間了。


Composer 源碼分析

下面我們通過對源碼的分析來看看 composer 是如何實現 PSR4標準 的自動加載功能。

很多框架在初始化的時候都會引入 composer 來協助自動加載的,以 Laravel 爲例,它入口文件 index.php 第一句就是利用 composer 來實現自動加載功能。

啓動

<?php
  define('LARAVEL_START', microtime(true));

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

去 vendor 目錄下的 autoload.php

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

  return ComposerAutoloaderInit7b790917ce8899df9af8ed53631a1c29::getLoader();

這裏就是 Composer 真正開始的地方了

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 標準的自動加載文件,

    • 存放着頂級命名空間與文件的映射;

autoload_real 引導類


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

<?php
    public static function getLoader()
    {
      if (null !== self::$loader) {
          return self::$loader;
      }

      spl_autoload_register(
        array('ComposerAutoloaderInit7b790917ce8899df9af8ed53631a1c29', 'loadClassLoader'), true, true
      );

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

      spl_autoload_unregister(
        array('ComposerAutoloaderInit7b790917ce8899df9af8ed53631a1c29', 'loadClassLoader')
      );

      $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION');

      if ($useStaticLoader) {
          require_once __DIR__ . '/autoload_static.php';

          call_user_func(
          \Composer\Autoload\ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::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\ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$files;
      } else {
          $includeFiles = require __DIR__ . '/autoload_files.php';
      }

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

      return $loader;
    }

我把自動加載引導類分爲 5 個部分。

第一部分——單例

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

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

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

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

<?php
  /***********************獲得自動加載核心類對象********************/
  spl_autoload_register(
    array('ComposerAutoloaderInit7b790917ce8899df9af8ed53631a1c29', 'loadClassLoader'), true, true
  );

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

  spl_autoload_unregister(
    array('ComposerAutoloaderInit7b790917ce8899df9af8ed53631a1c29', 'loadClassLoader')
  );

loadClassLoader()函數:

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

從程序裏面我們可以看出,composer 先向 PHP 自動加載機制註冊了一個函數,這個函數 require 了 ClassLoader 文件。成功 new 出該文件中核心類 ClassLoader() 後,又銷燬了該函數。

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

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

     call_user_func(
       \Composer\Autoload\ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::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 靜態初始化 ( PHP >= 5.6 )

靜態初始化只支持 PHP5.6 以上版本並且不支持 HHVM 虛擬機。我們深入 autoload_static.php 這個文件發現這個文件定義了一個用於靜態初始化的類,名字叫 ComposerStaticInit7b790917ce8899df9af8ed53631a1c29,仍然爲了避免衝突而加了 hash 值。這個類很簡單:

<?php
  class ComposerStaticInit7b790917ce8899df9af8ed53631a1c29{
     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
                          = ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$prefixLengthsPsr4;

          $loader->prefixDirsPsr4
                          = ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$prefixDirsPsr4;

          $loader->prefixesPsr0
                          = ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$prefixesPsr0;

          $loader->classMap
                          = ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$classMap;

      }, null, ClassLoader::class);
  }

這個靜態初始化類的核心就是 getInitializer() 函數,它將自己類中的頂級命名空間映射給了 ClassLoader 類。值得注意的是這個函數返回的是一個匿名函數,爲什麼呢?原因就是 ClassLoader類 中的 prefixLengthsPsr4prefixDirsPsr4等等變量都是 private的。利用匿名函數的綁定功能就可以將這些 private 變量賦給 ClassLoader 類 裏的成員變量。

關於匿名函數的綁定功能

接下來就是命名空間初始化的關鍵了。

classMap(命名空間映射)

<?php
  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',
  ...)

直接命名空間全名與目錄的映射,簡單粗暴,也導致這個數組相當的大。

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

<?php
  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 標準頂級命名空間映射用了兩個數組,第一個是用命名空間第一個字母作爲前綴索引,然後是 頂級命名空間,但是最終並不是文件路徑,而是 頂級命名空間的長度。爲什麼呢?

因爲 PSR4 標準是用頂級命名空間目錄替換頂級命名空間,所以獲得頂級命名空間的長度很重要。

具體說明這些數組的作用:

假如我們找 Symfony\Polyfill\Mbstring\example 這個命名空間,通過前綴索引和字符串匹配我們得到了

<?php
    'Symfony\\Polyfill\\Mbstring\\' => 26,

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

<?php
  '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 接口初始化( PHP < 5.6 )


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

<?php
    // 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);
    }

PSR4 標準的映射

autoload_psr4.php 的頂級命名空間映射

<?php
    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 標準的初始化接口:

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

總結下上面的頂級命名空間映射過程:

( 前綴 -> 頂級命名空間,頂級命名空間 -> 頂級命名空間長度 )
( 頂級命名空間 -> 目錄 )

這兩個映射數組。具體形式也可以查看下面的 autoload_static 的 $prefixLengthsPsr4 、 $prefixDirsPsr4 。

命名空間映射

autoload_classmap:

<?php
public static $classMap = array (
    'App\\Console\\Kernel'
        => __DIR__ . '/../..' . '/app/Console/Kernel.php',

    'App\\Exceptions\\Handler'
        => __DIR__ . '/../..' . '/app/Exceptions/Handler.php',
    ...
)

addClassMap:

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

自動加載核心類 ClassLoader 的靜態初始化到這裏就完成了!

其實說是5部分,真正重要的就兩部分——初始化與註冊。初始化負責頂層命名空間的目錄映射,註冊負責實現頂層以下的命名空間映射規則。

第四部分 —— 註冊


講完了 Composer 自動加載功能的啓動與初始化,經過啓動與初始化,自動加載核心類對象已經獲得了頂級命名空間與相應目錄的映射,也就是說,如果有命名空間 'App\Console\Kernel,我們已經可以找到它對應的類文件所在位置。那麼,它是什麼時候被觸發去找的呢?

這就是 composer 自動加載的核心了,我們先回顧一下自動加載引導類:

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

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

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

    return $loader;
} 

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

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

其實奧祕都在自動加載核心類 ClassLoader 的 loadClass() 函數上:

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

            return true;
        }
    }

這個函數負責按照 PSR 標準將頂層命名空間以下的內容轉爲對應的目錄,也就是上面所說的將 'App\Console\Kernel 中' Console\Kernel 這一段轉爲目錄,至於怎麼轉的在下面 “運行”的部分講。核心類 ClassLoader 將 loadClass() 函數註冊到PHP SPL中的 spl_autoload_register() 裏面去。這樣,每當PHP遇到一個不認識的命名空間的時候,PHP會自動調用註冊到 spl_autoload_register 裏面的 loadClass() 函數,然後找到命名空間對應的文件。

全局函數的自動加載

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

if ($useStaticLoader) {
    $includeFiles = Composer\Autoload\ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$files;
} else {
    $includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
    composerRequire7b790917ce8899df9af8ed53631a1c29($fileIdentifier, $file);
}

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

靜態初始化:

ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$files:

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

普通初始化

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',
   ....
);

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

加載全局函數

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

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

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

第五部分 —— 運行

到這裏,終於來到了核心的核心—— 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;
    }
}

最後小結

我們通過舉例來說下上面代碼的流程:

如果我們在代碼中寫下 new phpDocumentor\Reflection\Element(),PHP 會通過 SPL_autoload_register 調用 loadClass -> findFile -> findFileWithExtension。步驟如下:

  • 將 \ 轉爲文件分隔符/,加上後綴php,變成 $logicalPathPsr4, 即 phpDocumentor/Reflection//Element.php;
  • 利用命名空間第一個字母p作爲前綴索引搜索 prefixLengthsPsr4 數組,查到下面這個數組:
        p' => 
            array (
                'phpDocumentor\\Reflection\\' => 25,
                'phpDocumentor\\Fake\\' => 19,
          )
  • 遍歷這個數組,得到兩個頂層命名空間 phpDocumentor\Reflection\ 和 phpDocumentor\Fake\
  • 在這個數組中查找 phpDocumentor\Reflection\Element,找出 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(&dollar;logicalPathPsr4, &dollar;length)”文件是否存在,存在即返回。這裏就是
    '__DIR__/../phpdocumentor/reflection-common/src + substr(phpDocumentor/Reflection/Element.php,25)'
  • 如果失敗,則利用 fallbackDirsPsr4 數組裏面的目錄繼續判斷是否存在文件

以上就是 composer 自動加載的原理解析!

The end . Thanks!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章