對於php
的框架,不管是yii
,symfony
或者是laravel
,你們都在工做中有涉獵。對於在框架中的存放着資源包vendor
文件夾,入口文件(index.php
或者 app.php
),你們也都與他們天天碰面。可是你真的熟悉這些文件/文件夾嗎?一個完整的項目是如何從一個純淨框架發展而來?各個部分又在框架這個大廈中又起到了怎麼樣的做用?php
在上一章咱們說到了依賴注入,也不知道大夥都理解了沒有?不理解也沒問題,今天的這一章和上一章徹底沒有關係。laravel
如今咱們來到了下一個話題來,說說composer
這個工具。你們對於這個工具都不陌生,用它安裝插件真的是很是方便。可是他的原理你們是否清楚?原本就是一個普普統統的類,怎麼就被加載進來了呢?composer
說了,咱們欽定了,就由autoload
進行操做。json
這是一個特別重要的知識點。咱們常常會在框架的入口文件中看到它(__autoload
和spl_autoload_register
。固然如今你只能看到spl_auto_registe
r)。可是真的被問及這兩個方法的做用和方法的時候,大部分人仍是會一臉懵逼。數組
這兩個函數究竟是什麼?自動加載有又什麼方便之處?mvc
include
和 require
是PHP中引入文件的兩個基本方法。在小規模開發中直接使用 include
和 require
。 但在大型項目中會形成大量的 include 和 require 堆積。 (你想一想,一個文件裏面我寫幾百個include 你累不累?)app
這樣的代碼既不優雅,執行效率也很低,並且維護起來也至關困難。composer
爲了解決這個問題,部分框架會給出一個引入文件的配置清單,在對象初始化的時候把須要的文件引入。但這只是讓代碼變得更簡潔了一些,引入的效果仍然是差強人意。PHP5 以後,隨着 PHP 面向對象支持的完善,__autoload
函數才真正使得自動加載成爲可能。框架
在這裏我補充和當前章節無關的兩個知識點:yii
實現自動加載最簡單的方式就是使用 __autoload
魔術方法。當你引用不存在的類時,__autoload
就會被調用,而且你的類名會被做爲參數傳送過去。至於函數具體的邏輯,這須要用戶本身去實現。利用該性質,建立一個自動加載的機制。
首先建立一個 autoload.php
來作一個簡單的測試:函數
// 類未定義時,系統自動調用 function __autoload($class) { /* 具體處理邏輯 */ echo $class;// 簡單的輸出未定義的類名 } new HelloWorld(); /** * 輸出 HelloWorld 與報錯信息 * Fatal error: Class 'HelloWorld' not found */ 經過這個簡單的例子能夠發現,在類的實例化過程當中,系統所作的工做大體是這樣的: /* 模擬系統實例化過程 */ function instance($class) { // 若是類存在則返回其實例 if (class_exists($class, false)) { return new $class(); } // 查看 autoload 函數是否被用戶定義 if (function_exists('__autoload')) { __autoload($class); // 最後一次引入的機會 } // 再次檢查類是否存在 if (class_exists($class, false)) { return new $class(); } else { // 系統:我實在沒轍了 throw new Exception('Class Not Found'); } }
明白了 __autoload
函數的工做原理以後,那就讓咱們來用它去實現自動加載。
首先建立一個類文件(建議文件名與類名一致),代碼以下:
class [ClassName] { // 對象實例化時輸出當前類名 function __construct() { echo '<h1>' . __CLASS__ . '</h1>'; } }
(我這裏建立了一個 HelloWorld
類用做演示)接下來咱們就要定義 __autoload
的具體邏輯,使它可以實現自動加載:
function __autoload($class) { // 根據類名肯定文件名 $file = $class . '.php'; if (file_exists($file)) { include $file; // 引入PHP文件 } } new HelloWorld(); /** * 輸出 <h1>HelloWorld</h1> */
看上去很美好對吧?利用這個__autoload
就能寫一個自動加載類的機制。可是你有沒有試過在一個文件裏面寫兩個__autoload
? 不用想,結果報錯。在一個大型框架中,你敢保障你只有一個__autoload
?這樣不就很麻煩嗎?
不用着急,spl_autoload_register()
該出場了。不過再解釋以前,咱們得說另一個重要的概念--命名空間。
其實命名空間並非什麼新生事物,不少語言(例如C++)早都支持這個特性了。只不過 PHP 起步比較晚,直到 PHP 5.3
以後才支持。命名空間簡而言之就是一種標識,它的主要目的是解決命名衝突的問題。
就像在平常生活中,有不少姓名相同的人,如何區分這些人呢?那就須要加上一些額外的標識。把工做單位當成標識彷佛不錯,這樣就不用擔憂 「撞名」 的尷尬了。
這裏咱們來作一個小任務,去介紹百度的CEO李彥宏:
namespace 百度; class 李彥宏 { function __construct() { echo '百度創始人'; } }
這就是李彥宏的基本資料了,namespace
是他的單位標識,class
是他的姓名。命名空間經過關鍵字 namespace
來聲明。若是一個文件中包含命名空間,它必須在其它全部代碼以前聲明命名空間。
new 百度\李彥宏(); // 限定類名 new \百度\李彥宏(); // 徹底限定類名
在通常狀況下,不管是向別人介紹 "百度 李彥宏"
仍是 "百度公司 李彥宏"
,他們都可以明白。在當前命名空間沒有聲明的狀況下,限定類名和徹底限定類名是等價的。由於若是不指定空間,則默認爲全局()。
namespace 谷歌; new 百度\李彥宏(); // 谷歌\百度\李彥宏(實際結果) new \百度\李彥宏(); // 百度\李彥宏(實際結果)
若是你在谷歌公司向他們的員工介紹李彥宏,必定要指明是 "百度公司的李彥宏"。不然他會認爲百度是谷歌的一個部門,而李彥宏只是其中的一位員工而已。這個例子展現了在命名空間下,使用限定類名和徹底限定類名的區別。(徹底限定類名 = 當前命名空間 + 限定類名)
/* 導入命名空間 */ use 百度\李彥宏; new 李彥宏(); // 百度\李彥宏(實際結果) /* 設置別名 */ use 百度\李彥宏 AS CEO; new CEO(); // 百度\李彥宏(實際結果) /* 任何狀況 */ new \百度\李彥宏();// 百度\李彥宏(實際結果)
第一種狀況是別人已經認識李彥宏了,你只須要直接說名字,他就能知道你指的是誰。第二種狀況是李彥宏就是他們的CEO,你直接說CEO,他能夠馬上反應過來。使用命名空間只是讓類名有了前綴,不容易發生衝突,系統仍然不會進行自動導入。
若是不引入文件,系統會在拋出 "Class Not Found" 錯誤以前觸發 __autoload
函數,並將限定類名傳入做爲參數。
因此上面的例子都是基於你已經將相關文件手動引入的狀況下實現的,不然系統會拋出 " Class '百度李彥宏' not found"。
接下來讓咱們要在含有命名空間的狀況下去實現自動加載。這裏咱們使用 spl_autoload_register()
函數來實現,這須要你的 PHP 版本號大於 5.12。spl_autoload_register
函數的功能就是把傳入的函數(參數能夠爲回調函數或函數名稱形式)註冊到 SPL __autoload
函數隊列中,並移除系統默認的 __autoload()
函數。一旦調用 spl_autoload_register()
函數,當調用未定義類時,系統就會按順序調用註冊到 spl_autoload_register()
函數的全部函數,而不是自動調用 __autoload()
函數。
如今,咱們來建立一個 Linux
類,它使用 os
做爲它的命名空間(建議文件名與類名保持一致):
namespace os; // 命名空間 class Linux // 類名 { function __construct() { echo '<h1>' . __CLASS__ . '</h1>'; } }
接着,在同一個目錄下新建一個 PHP 文件,使用 spl_autoload_register
以函數回調的方式實現自動加載:
spl_autoload_register(function ($class) { // class = os\Linux /* 限定類名路徑映射 */ $class_map = array( // 限定類名 => 文件路徑 'os\\Linux' => './Linux.php', ); /* 根據類名肯定文件名 */ $file = $class_map[$class]; /* 引入相關文件 */ if (file_exists($file)) { include $file; } }); new \os\Linux();
這裏咱們使用了一個數組去保存類名與文件路徑的關係,這樣當類名傳入時,自動加載器就知道該引入哪一個文件去加載這個類了。
可是一旦文件多起來的話,映射數組會變得很長,這樣的話維護起來會至關麻煩。若是命名能遵照統一的約定,就可讓自動加載器自動解析判斷類文件所在的路徑。接下來要介紹的PSR-4
就是一種被普遍採用的約定方式。
PSR-4
是關於由文件路徑自動載入對應類的相關規範,規範規定了一個徹底限定類名須要具備如下結構:
\<頂級命名空間>(\<子命名空間>)*\<類名>
若是繼續拿上面的例子打比方的話,頂級命名空間至關於公司,子命名空間至關於職位,類名至關於人名。那麼李彥宏標準的稱呼爲 "百度公司 CEO 李彥宏"。
PSR-4 規範中必需要有一個頂級命名空間,它的意義在於表示某一個特殊的目錄(文件基目錄)。子命名空間表明的是類文件相對於文件基目錄的這一段路徑(相對路徑),類名則與文件名保持一致(注意大小寫的區別)。
舉個例子:在全限定類名 \app\view\news\Index
中,若是 app
表明 C:\Baidu
,那麼這個類的路徑則是 C:\Baidu\view\news\Index.php
咱們就以解析 \app\view\news\Index
爲例,編寫一個簡單的 Demo
:
$class = 'app\view\news\Index'; /* 頂級命名空間路徑映射 */ $vendor_map = array( 'app' => 'C:\Baidu', ); /* 解析類名爲文件路徑 */ $vendor = substr($class, 0, strpos($class, '\\')); // 取出頂級命名空間[app] $vendor_dir = $vendor_map[$vendor]; // 文件基目錄[C:\Baidu] $rel_path = dirname(substr($class, strlen($vendor))); // 相對路徑[/view/news] $file_name = basename($class) . '.php'; // 文件名[Index.php] /* 輸出文件所在路徑 */ echo $vendor_dir . $rel_path . DIRECTORY_SEPARATOR . $file_name;
經過這個 Demo
能夠看出限定類名轉換爲路徑的過程。那麼如今就讓咱們用規範的面向對象方式去實現自動加載器吧。
首先咱們建立一個文件 Index.php
,它處於 \app\mvc\view\home
目錄中:
namespace app\mvc\view\home; class Index { function __construct() { echo '<h1> Welcome To Home </h1>'; } }
接着咱們在建立一個加載類(不須要命名空間),它處於 目錄中:
class Loader { /* 路徑映射 */ public static $vendorMap = array( 'app' => __DIR__ . DIRECTORY_SEPARATOR . 'app', ); /** * 自動加載器 */ public static function autoload($class) { $file = self::findFile($class); if (file_exists($file)) { self::includeFile($file); } } /** * 解析文件路徑 */ private static function findFile($class) { $vendor = substr($class, 0, strpos($class, '\\')); // 頂級命名空間 $vendorDir = self::$vendorMap[$vendor]; // 文件基目錄 $filePath = substr($class, strlen($vendor)) . '.php'; // 文件相對路徑 return strtr($vendorDir . $filePath, '\\', DIRECTORY_SEPARATOR); // 文件標準路徑 } /** * 引入文件 */ private static function includeFile($file) { if (is_file($file)) { include $file; } } }
最後,將 Loader
類中的 autoload
註冊到 spl_autoload_register
函數中:
include 'Loader.php'; // 引入加載器 spl_autoload_register('Loader::autoload'); // 註冊自動加載 new \app\mvc\view\home\Index(); // 實例化未引用的類 /** * 輸出: <h1> Welcome To Home </h1> */
說了這麼多,終於該composer
登場啦。關於安裝之類的我在這裏就不在贅述了。下面來看看vendor/composer
的文件詳情
vendor ----autoload_classmap.php ----autoload_files.php ----autoload_namespace.php ----autoload_psr4.php ----autoload_real.php ----autoload_static.php ----ClassLoader.php ----install.json autoload.php
那麼我先看看vendor/autoload.php
:
<?php // autoload.php @generated by Composer require_once __DIR__ . '/composer' . '/autoload_real.php'; return ComposerAutoloaderInitff1d77c91141523097b07ee2acc23326::getLoader(); 其執行了一個自動生成的類ComposerAutoloaderInitff1d77c91141523097b07ee2acc23326中的getLoader方法。 咱們跟進到autoload_real.php上。 public static function getLoader() { if (null !== self::$loader) { return autoload_real.phpself::$loader; } spl_autoload_register(array('ComposerAutoloaderInitff1d77c91141523097b07ee2acc23326', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(); spl_autoload_unregister(array('ComposerAutoloaderInitff1d77c91141523097b07ee2acc23326', 'loadClassLoader')); $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); $includeFiles = require __DIR__ . '/autoload_files.php'; foreach ($includeFiles as $file) { composerRequireff1d77c91141523097b07ee2acc23326($file); } return $loader; }
能夠明顯看到,他將autoload_namespaces.php
、autoload_psr4.php
、autoload_classmap.php
、autoload_files.php
等幾個配置文件包含了進來,並進行了相關處理(setPsr4
),最後註冊(register
)。
那麼咱們跟進register
方法:
public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); }
這函數就一行,但簡單明瞭,直接調用php自帶的spl_autoload_register
函數,註冊處理__autoload
的方法,也就是loadClass
方法。再跟進loadClass
方法:
public function loadClass($class) { if ($file = $this->findFile($class)) { includeFile($file); return true; } }
從函數名字就能夠大概知道流程:若是存在$class
對應的這個$file
,則include進來。
那麼進findFile方法裏看看吧:
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; }
經過類名找文件,最終鎖定在findFileWithExtension
方法中。
仍是跟進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; } }
最終實現將命名空間\類
這樣的類名,給轉換成目錄名/類名.php
這樣的路徑,並返回完整路徑。
我發現composer
的autoload
與php
自帶的spl_autoload
,在包含文件時有一點小區別。那就是,spl_autoload
會查找.inc
類型的文件名,但composer
不會。
另外也能夠發現,雖然配置文件的名字是autoload_psr4.php
,但實際上psr0
格式的自動加載也是支持的。兩者最大的不一樣就是psr0
中用」_」來代替目錄間的」」。
以上說了這麼多,也該總結一下了。從__autoload
到spl_autoload_register
再到composer
和psr4
方法。php官方和社區設計了這麼多都是爲了什麼?它們就是爲了解決include
文件不方便的問題。說一千道一萬,原來一個個的include
不方便,我如今使用spl_autoload_register
直接自動include
了。可是咱們不能瞎寫,還要有規則,因而就有了psr4
。