前言
PHP 自5.3的版本以後,已經重煥新生,命名空間、性狀(trait)、閉包、接口、PSR 規範、以及 composer 的出現已經讓 PHP 變成了一門現代化的腳本語言。PHP 的生態系統也一直在演進,而 composer 的出現更是完全的改變了以往構建 PHP 應用的方式,咱們能夠根據 PHP 的應用需求混合搭配最合適的 PHP 組件。固然這也得益於 PSR 規範的提出。css
大綱
- PHP 自動加載功能
- PSR 規範
- comoposer 的自動加載過程
- composer 源碼分析
1、PHP 自動加載功能
PHP 自動加載功能的由來
在 PHP 開發過程當中,若是但願從外部引入一個 Class ,一般會使用 include
和 require
方法,去把定義這個 Class 的文件包含進來。這個在小規模開發的時候,沒什麼大問題。但在大型的開發項目中,使用這種方式會帶來一些隱含的問題:若是一個 PHP 文件須要使用不少其它類,那麼就須要不少的 require/include
語句,這樣有可能會 形成遺漏 或者 包含進沒必要要的類文件。若是大量的文件都須要使用其它的類,那麼要保證每一個文件都包含正確的類文件確定是一個噩夢, 何況 require或 incloud 的性能代價很大。程序員
PHP5 爲這個問題提供了一個解決方案,這就是 類的自動加載(autoload)機制
。autoload機制
可使得 PHP 程序有可能在使用類時才自動包含類文件,而不是一開始就將全部的類文件include
進來,這種機制也稱爲 Lazy loading (惰性加載)
。web
-
總結起來,自動加載功能帶來了幾處優勢:json
- 使用類以前無需
include / require
- 使用類的時候纔會
include / require
文件,實現了lazy loading
,避免了include / require
多餘文件。 - 無需考慮引入 類的實際磁盤地址 ,實現了邏輯和實體文件的分離。
- 使用類以前無需
PHP 自動加載函數 __autoload()
-
從 PHP5 開始,當咱們在使用一個類時,若是發現這個類沒有加載,就會自動運行 __autoload() 函數,這個函數是咱們在程序中自定義的,在這個函數中咱們能夠加載須要使用的類。下面是個簡單的示例:bootstrap
<?php function __autoload($classname) { require_once ($classname . ".class.php"); }
-
在咱們這個簡單的例子中,咱們直接將類名加上擴展名
.class.php
構成了類文件名,而後使用require_once
將其加載。segmentfault從這個例子中,咱們能夠看出 __autoload 至少要作三件事情:數組
- 根據類名肯定類文件名;
- 肯定類文件所在的磁盤路徑;
- 將類從磁盤文件中加載到系統中。
- 第三步最簡單,只須要使用
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() 函數。緩存
2、PSR 規範
與自動加載相關的規範是 PSR4,在說 PSR4 以前先介紹一下 PSR 標準。PSR 標準的發明和推出組織是:PHP-FIG,它的網站是:www.php-fig.org。由幾位開源框架的開發者成立於 2009 年,從那開始也選取了不少其餘成員進來,雖然不是 「官方」 組織,但也表明了社區中不小的一塊。組織的目的在於:以最低程度的限制,來統一各個項目的編碼規範,避免各家自行發展的風格阻礙了程序員開發的困擾,因而大夥發明和總結了 PSR,PSR 是 PHP Standards Recommendation 的縮寫,截止到目前爲止,總共有 14 套 PSR 規範,其中有 7 套PSR規範已經過表決並推出使用,分別是:ruby
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自動加載所用到的源文件。
autoload_real.php: 自動加載功能的引導類。
- composer 加載類的初始化
(頂級命名空間與文件路徑映射初始化)
和註冊(spl_autoload_register())。ClassLoader.php : composer 加載類。
- composer 自動加載功能的核心類。
autoload_static.php : 頂級命名空間初始化類,
- 用於給核心類初始化頂級命名空間。
autoload_classmap.php : 自動加載的最簡單形式,
- 有完整的命名空間和文件目錄的映射;
autoload_files.php : 用於加載全局函數的文件,
- 存放各個全局函數所在的文件路徑名;
autoload_namespaces.php : 符合 PSR0 標準的自動加載文件,
- 存放着頂級命名空間與文件的映射;
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類
中的 prefixLengthsPsr4
、prefixDirsPsr4
等等變量都是 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($logicalPathPsr4, $length)」文件是否存在,存在即返回。這裏就是
'__DIR__/../phpdocumentor/reflection-common/src + substr(phpDocumentor/Reflection/Element.php,25)'
- 若是失敗,則利用 fallbackDirsPsr4 數組裏面的目錄繼續判斷是否存在文件
以上就是 composer 自動加載的原理解析!
另注意:若是本身在使用第三方包的過程當中,本身定義的根命名空間和第三方包的命名空間同樣,則會以數組的形式存儲,如
'Fairy\\' => array($baseDir . '/app', $vendorDir . '/cshaptx4869/work/src/Fairy'),