上一篇文章,咱們討論了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 Monolog\Logger便可,神奇吧! 簡單的說,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,而要這麼麻煩?緣由就是怕有的用戶也定義了個\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)調用核心類接口初始化。
靜態初始化只支持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.
原文連接:www.zhanggaoyuan.com/article/30
原文標題:[Composer 的 Autoload 源碼實現 - 啓動與初始化]
本站使用「 署名-非商業性使用 4.0 國際 (CC BY-NC 4.0)」創做共享協議,轉載或使用請署名並註明出處。
本篇文章由一文多發平臺ArtiPub自動發佈