淺析PHP類(lèi)的自動(dòng)加載和命名空間
php是使用require(require_once)和include(include_once)關(guān)鍵字加載類(lèi)文件。但是在實(shí)際的開(kāi)發(fā)工程中我們基本上不會(huì)去使用這些關(guān)鍵字去加載類(lèi)。 因?yàn)檫@樣做會(huì)使得代碼的維護(hù)相當(dāng)?shù)睦щy。實(shí)際的開(kāi)發(fā)中我們會(huì)在文件的開(kāi)始位置用use關(guān)鍵字使用類(lèi),然后直接new這個(gè)類(lèi)就可以了. 至于類(lèi)是怎么加載的,一般都是框架或者composer去實(shí)現(xiàn)的。
<?php use IlluminateContainerContainer; $container = new Container();
自動(dòng)加載
我們可以通過(guò)一段偽代碼來(lái)模擬一下在類(lèi)的實(shí)例化工程中類(lèi)是如何工作的
function instance($class) { // 如果類(lèi)已加載則返回其實(shí)例 if (class_exists($class, false)) { return new $class(); } // 查看 autoload 函數(shù)是否被用戶(hù)定義 if (function_exists('__autoload')) { __autoload($class); // 最后一次加載類(lèi)的機(jī)會(huì) } // 再次檢查類(lèi)是否存在 if (class_exists($class, false)) { return new $class(); } else { // 系統(tǒng):我實(shí)在沒(méi)轍了 throw new Exception('Class Not Found'); } }
php在語(yǔ)言層面提供了**__autoload** 魔術(shù)方法給用戶(hù)來(lái)實(shí)現(xiàn)自己的自動(dòng)加載邏輯。當(dāng)用戶(hù)去new一個(gè)類(lèi)的時(shí)候,如果該類(lèi)沒(méi)有被加載,php會(huì)在拋出錯(cuò)誤前調(diào)用**__autoload方法去加載類(lèi)。下面的例子中的__autoload**方法只是簡(jiǎn)單的輸出要加載類(lèi)的名稱(chēng), 并沒(méi)有去實(shí)際的加載對(duì)應(yīng)的類(lèi), 所以會(huì)拋出錯(cuò)誤。
<?php use IlluminateContainerContainer; $container = new Container(); function __autoload($class) { /* 具體處理邏輯 */ echo $class;// 簡(jiǎn)單的輸出要加載類(lèi)的名稱(chēng) } /** *
運(yùn)行結(jié)果
IlluminateContainerContainer Fatal error: Uncaught Error: Class 'IlluminateContainerContainer' not found in D:projectphplaravel_for_ci_cdtestClassLoader.php:5 Stack trace: #0 {main} thrown in D:projectphplaravel_for_ci_cdtestClassLoader.php on line 5 */
明白了 **__autoload** 函數(shù)的工作原理之后,我們來(lái)用它去實(shí)現(xiàn)一個(gè)最簡(jiǎn)單自動(dòng)加載。我們會(huì)有index.php和Person.php兩個(gè)文件在同一個(gè)目錄下。
//index.php <?php function __autoload($class) { // 根據(jù)類(lèi)名確定文件名 $file = './'.$class . '.php'; if (file_exists($file)) { include $file; // 引入PHP文件 } } new Person(); /*---------------------分割線(xiàn)-------------------------------------*/ //Person.php class Person { // 對(duì)象實(shí)例化時(shí)輸出當(dāng)前類(lèi)名 function __construct() { echo '<h1>' . __CLASS__ . '</h1>'; } } /**運(yùn)行結(jié)果 * 輸出 <h1>Person</h1> */
命名空間
命名空間并不是什么新鮮的事務(wù),很多語(yǔ)言都早就支持了這個(gè)特性(只是叫法不相同),它主要解決的一個(gè)問(wèn)題就是命名沖突! 就好像日常生活中很多人都會(huì)重名,我們必須要通過(guò)一些標(biāo)識(shí)來(lái)區(qū)分他們的不同。比如說(shuō)現(xiàn)在我們要用php介紹一個(gè)叫張三的人 ,他在財(cái)務(wù)部門(mén)工作。我們可以這樣描述。
namespace 財(cái)務(wù)部門(mén); class 張三 { function __construct() { echo '財(cái)務(wù)部門(mén)的張三'; } }
這就是張三的基本資料 , namespace是他的部門(mén)標(biāo)識(shí),class是他的名稱(chēng). 這樣大家就可以知道他是財(cái)務(wù)部門(mén)的張三而不是工程部門(mén)的張三。
非限定名稱(chēng),限定名稱(chēng)和完全限定名稱(chēng)
1.非限定名稱(chēng),或不包含前綴的類(lèi)名稱(chēng),例如 $comment = new Comment(); 如果當(dāng)前命名空間是BlogArticle,Comment將被解析為、BlogArticleComment。如果使用Comment的代碼不包含在任何命名空間中的代碼(全局空間中),則Comment會(huì)被解析為Comment。
注意: 如果文件的開(kāi)頭有使用use關(guān)鍵字 use onetwoComment; 則Comment會(huì)被解析為 **onetwoComment**。
2.限定名稱(chēng),或包含前綴的名稱(chēng),例如 $comment = new ArticleComment(); 如果當(dāng)前的命名空間是Blog,則Comment會(huì)被解析為BlogArticleComment。如果使用Comment的代碼不包含在任何命名空間中的代碼(全局空間中),則Comment會(huì)被解析為ArticleComment。
3.完全限定名稱(chēng),或包含了全局前綴操作符的名稱(chēng),例如 $comment = new ArticleComment(); 在這種情況下,Comment總是被解析為ArticleComment。
spl_autoload
接下來(lái)讓我們要在含有命名空間的情況下去實(shí)現(xiàn)類(lèi)的自動(dòng)加載。我們使用 spl_autoload_register() 函數(shù)來(lái)實(shí)現(xiàn),這需要你的 PHP 版本號(hào)大于 5.12。spl_autoload_register函數(shù)的功能就是把傳入的函數(shù)(參數(shù)可以為回調(diào)函數(shù)或函數(shù)名稱(chēng)形式)注冊(cè)到 SPL __autoload 函數(shù)隊(duì)列中,并移除系統(tǒng)默認(rèn)的 **__autoload()** 函數(shù)。一旦調(diào)用 spl_autoload_register() 函數(shù),當(dāng)調(diào)用未定義類(lèi)時(shí),系統(tǒng)就會(huì)按順序調(diào)用注冊(cè)到 spl_autoload_register() 函數(shù)的所有函數(shù),而**不是自動(dòng)調(diào)用 __autoload()** 函數(shù)。
現(xiàn)在, 我們來(lái)創(chuàng)建一個(gè) Linux 類(lèi),它使用 os 作為它的命名空間(建議文件名與類(lèi)名保持一致):
<?php namespace os; // 命名空間 class Linux // 類(lèi)名 { function __construct() { echo '<h1>' . __CLASS__ . '</h1>'; } }
接著,在同一個(gè)目錄下新建一個(gè) index.php文件,使用 spl_autoload_register 以函數(shù)回調(diào)的方式實(shí)現(xiàn)自動(dòng)加載:
<?php spl_autoload_register(function ($class) { // class = osLinux /* 限定類(lèi)名路徑映射 */ $class_map = array( // 限定類(lèi)名 => 文件路徑 'os\Linux' => './Linux.php', ); /* 根據(jù)類(lèi)名確定文件路徑 */ $file = $class_map[$class]; /* 引入相關(guān)文件 */ if (file_exists($file)) { include $file; } }); new osLinux();
這里我們使用了一個(gè)數(shù)組去保存類(lèi)名與文件路徑的關(guān)系,這樣當(dāng)類(lèi)名傳入時(shí),自動(dòng)加載器就知道該引入哪個(gè)文件去加載這個(gè)類(lèi)了。但是一旦文件多起來(lái)的話(huà),映射數(shù)組會(huì)變得很長(zhǎng),這樣的話(huà)維護(hù)起來(lái)會(huì)相當(dāng)麻煩。如果命名能遵守統(tǒng)一的約定,就可以讓自動(dòng)加載器自動(dòng)解析判斷類(lèi)文件所在的路徑。接下來(lái)要介紹的PSR-4 就是一種被廣泛采用的約定方式
PSR-4規(guī)范
PSR-4 是關(guān)于由文件路徑自動(dòng)載入對(duì)應(yīng)類(lèi)的相關(guān)規(guī)范,規(guī)范規(guī)定了一個(gè)完全限定類(lèi)名需要具有以下結(jié)構(gòu):
<頂級(jí)命名空間>(<子命名空間>)*<類(lèi)名>
PSR-4 規(guī)范中必須要有一個(gè)頂級(jí)命名空間,它的意義在于表示某一個(gè)特殊的目錄(文件基目錄)。子命名空間代表的是類(lèi)文件相對(duì)于文件基目錄的這一段路徑(相對(duì)路徑),類(lèi)名則與文件名保持一致(注意大小寫(xiě)的區(qū)別)。
舉個(gè)例子:在全限定類(lèi)名 appviewnewsIndex 中,如果 app 代表 C:Baidu,那么這個(gè)類(lèi)的路徑則是 C:BaiduviewnewsIndex.php.我們就以解析 appviewnewsIndex 為例,編寫(xiě)一個(gè)簡(jiǎn)單的 Demo:
<?php $class = 'appviewnewsIndex'; /* 頂級(jí)命名空間路徑映射 */ $vendor_map = array( 'app' => 'C:Baidu', ); /* 解析類(lèi)名為文件路徑 */ $vendor = substr($class, 0, strpos($class, '\')); // 取出頂級(jí)命名空間[app] $vendor_dir = $vendor_map[$vendor]; // 文件基目錄[C:Baidu] $rel_path = dirname(substr($class, strlen($vendor))); // 相對(duì)路徑[/view/news] $file_name = basename($class) . '.php'; // 文件名[Index.php] /* 輸出文件所在路徑 */ echo $vendor_dir . $rel_path . DIRECTORY_SEPARATOR . $file_name;