在開始之前,歡迎關注我自己的部落格: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自動載入所用到的原始檔。
- 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來實現自動載入功能。
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 協議》,轉載必須註明作者和本文連結