上一篇文章,咱們討論了PHP的自動加載原理、PHP的命名空間、PHP的PSR0與PSR4標準,有了這些知識,其實咱們就能夠按照PSR4標準寫出能夠自動加載的程序了。然而咱們爲何要本身寫呢?尤爲是有Composer這神同樣的包管理器的狀況下?php
Composer 是 PHP 的一個依賴管理工具。它容許你申明項目所依賴的代碼庫,它會在你的項目中爲你安裝他們。詳細內容能夠查看Composer 中文網。
Composer Composer 將這樣爲你解決問題:css
- 你有一個項目依賴於若干個庫。
- 其中一些庫依賴於其餘庫。
- 你聲明你所依賴的東西。
- Composer 會找出哪一個版本的包須要安裝,並安裝它們(將它們下載到你的項目中)。
例如,你正在建立一個項目,你須要一個庫來作日誌記錄。你決定使用 monolog。爲了將它添加到你的項目中,你所須要作的就是建立一個 composer.json 文件,其中描述了項目的依賴關係。html
{ "require": { "monolog/monolog": "1.2.*" } }
而後咱們只要在項目裏面直接use MonologLogger便可,神奇吧!
簡單的說,Composer幫助咱們下載好了符合PSR0或PSR4標準的第三方庫,並把文件放在相應位置;幫咱們寫了_autoload()函數,註冊到了spl_register()函數,當咱們想用第三方庫的時候直接使用命名空間便可。
那麼當咱們想要寫本身的命名空間的時候,該怎麼辦呢?很簡單,咱們只要按照PSR4標準命名咱們的命名空間,放置咱們的文件,而後在composer裏面寫好頂級域名與具體目錄的映射,就能夠享用composer的便利了。
固然若是有一個很是棒的框架,咱們會驚喜地發現,在composer裏面寫頂級域名映射這事咱們也不用作了,框架已經幫咱們寫好了頂級域名映射了,咱們只須要在框架裏面新建文件,在新建的文件中寫好命名空間,就能夠在任何地方use咱們的命名空間了。
下面咱們就以laravel框架爲例,講一講composer是如何實現PSR0和PSR4標準的自動加載功能。laravel
首先,咱們先大體瞭解一下Composer自動加載所用到的源文件。git
- 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標準的自動加載文件,存放着頂級命名空間與文件的映射;
laravel框架的初始化是須要composer自動加載協助的,因此laravel的入口文件index.php第一句就是利用composer來實現自動加載功能。github
require __DIR__.'/../bootstrap/autoload.php';
我們接着去看bootstrap目錄下的autoload.php:web
define('LARAVEL_START', microtime(true)); require __DIR__.'/../vendor/autoload.php';
再去vendor目錄下的autoload.php:json
require_once __DIR__ . '/composer' . '/autoload_real.php'; return ComposerAutoloaderInit 832ea71bfb9a4128da8660baedaac82e::getLoader();
爲何框架要在bootstrap/autoload.php轉一下?我的理解,laravel這樣設計有利於支持或擴展任意有自動加載的第三方庫。
好了,咱們終於要看到了Composer真正要顯威的地方了。autoload_real裏面就是一個自動加載功能的引導類,這個類不負責具體功能邏輯,只作了兩件事:初始化自動加載類、註冊自動加載類。
到autoload_real這個文件裏面去看,發現這個引導類的名字叫ComposerAutoloaderInit832ea71bfb9a4128da8660baedaac82e,爲何要叫這麼古怪的名字呢?由於這是防止用戶自定義類名跟這個類重複衝突了,因此在類名上加了一個hash值。其實還有一個作法咱們更加熟悉,那就是不直接定義類名,而是定義一個命名空間。這裏爲何不定義一個命名空間呢?我的理解:命名空間通常都是爲了複用,而這個類只須要運行一次便可,之後也不會用獲得,用hash值更加合適。bootstrap
在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; }
第二部分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,而要這麼麻煩?緣由就是怕有的用戶也定義了個ComposerAutoloadClassLoader命名空間,致使自動加載錯誤文件。那爲何不跟引導類同樣用個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)調用核心類接口初始化。
靜態初始化只支持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類中的prefixLengthsPsr四、prefixDirsPsr4等等都是private的。。。普通的函數沒辦法給類的private成員變量賦值。利用匿名函數的綁定功能就能夠將把匿名函數轉爲ClassLoader類的成員函數。關於匿名函數的綁定功能。
接下來就是頂級命名空間初始化的關鍵了。
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', ...)
簡單吧,直接命名空間全名與目錄的映射,沒有頂級命名空間。。。簡單粗暴,也致使這個數組至關的大。
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是否存在,若是不存在接着遍歷數組(這個例子數組只有一個元素),若是數組遍歷完都沒有,就會加載失敗。
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的靜態初始化完成!!!
若是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); }
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。若是沒有頂級命名空間,就只存儲一個路徑名,以便在後面嘗試加載。
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.
原文連接:https://www.zhanggaoyuan.com/article/30
原文標題:[Composer 的 Autoload 源碼實現 - 啓動與初始化]
本站使用「 署名-非商業性使用 4.0 國際 (CC BY-NC 4.0)」創做共享協議,轉載或使用請署名並註明出處。
本篇文章由一文多發平臺ArtiPub自動發佈