摘要:include() 和 require() 是PHP中引入文件的兩個(gè)原始方法,但如果在項(xiàng)目中大量使用這兩個(gè)函數(shù)的話,代碼既不優(yōu)雅,執(zhí)行效率也很低,而且維護(hù)起來(lái)也相當(dāng)困難,于是自動(dòng)加載由此而生。在使用PHP命名空間之前...
include() 和 require() 是PHP中引入文件的兩個(gè)原始方法,但如果在項(xiàng)目中大量使用這兩個(gè)函數(shù)的話,代碼既不優(yōu)雅,執(zhí)行效率也很低,而且維護(hù)起來(lái)也相當(dāng)困難,于是自動(dòng)加載由此而生。
使用 __autoload 實(shí)現(xiàn)文件自動(dòng)加載
在使用PHP命名空間之前(PHP 5.3以下),為了實(shí)現(xiàn)文件的自動(dòng)加載,大部分都是采用 __autoload() 魔術(shù)方法。當(dāng)需要使用的類沒有被引入時(shí),這個(gè)函數(shù)會(huì)在PHP報(bào)錯(cuò)前被觸發(fā),未定義的類名會(huì)被當(dāng)作參數(shù)傳入。至于函數(shù)具體的邏輯,這需要用戶自己去實(shí)現(xiàn)。
首先創(chuàng)建一個(gè) autoload.php 來(lái)做一個(gè)簡(jiǎn)單的測(cè)試:
// 類未定義時(shí),系統(tǒng)自動(dòng)調(diào)用 function __autoload($class){ /* 具體處理邏輯 */ echo $class;// 簡(jiǎn)單的輸出未定義的類名 } new HelloWorld(); /** * 輸出 HelloWorld 與報(bào)錯(cuò)信息 * Fatal error: Class 'HelloWorld' not found */
通過(guò)這個(gè)簡(jiǎn)單的例子可以發(fā)現(xiàn),在類的實(shí)例化過(guò)程中,系統(tǒng)所做的工作大致是這樣的:
/* 模擬系統(tǒng)實(shí)例化過(guò)程 */ function instance($class){ // 如果類存在則返回其實(shí)例 if (class_exists($class, false)) { return new $class(); } // 查看 autoload 函數(shù)是否被用戶定義 if (function_exists('__autoload')) { __autoload($class); // 最后一次引入的機(jī)會(huì) } // 再次檢查類是否存在 if (class_exists($class, false)) { return new $class(); } else { // 系統(tǒng):我實(shí)在沒轍了 throw new Exception('Class Not Found'); } }
__autoload()自動(dòng)加載原理非常簡(jiǎn)單,這里我們只是拋磚引玉,我們主要來(lái)講 命名空間下的 自動(dòng)加載原理。
在PHP 5.3以后引入了命名空間的特性,命名空間簡(jiǎn)而言之就是一種標(biāo)識(shí),它的主要目的是解決命名沖突的問(wèn)題,就像在日常生活中,有很多姓名相同的人,如何區(qū)分這些人呢?那就需要加上一些額外的標(biāo)識(shí)。
把工作單位當(dāng)成標(biāo)識(shí)似乎不錯(cuò),這樣就不用擔(dān)心 “撞名” 的尷尬了。
這里我們來(lái)做一個(gè)小任務(wù),去介紹百度的CEO李彥宏:
namespace 百度; class 李彥宏 { function __construct() { echo '百度創(chuàng)始人'; } }
↑ 這就是李彥宏的基本資料了,namespace 是他的單位標(biāo)識(shí),class 是他的姓名。
命名空間通過(guò)關(guān)鍵字 namespace 來(lái)聲明。如果一個(gè)文件中包含命名空間,它必須在其它所有代碼之前聲明命名空間。
new 百度\李彥宏(); // 限定類名 new \百度\李彥宏(); // 完全限定類名
↑ 在一般情況下,無(wú)論是向別人介紹 "百度 李彥宏" 還是 "百度公司 李彥宏",他們都能夠明白。
在當(dāng)前命名空間沒有聲明的情況下,限定類名和完全限定類名是等價(jià)的。因?yàn)槿绻恢付臻g,則默認(rèn)為全局(\)。
namespace 谷歌; new 百度\李彥宏(); // 谷歌\百度\李彥宏(實(shí)際結(jié)果) new \百度\李彥宏(); // 百度\李彥宏(實(shí)際結(jié)果)
↑ 如果你在谷歌公司向他們的員工介紹李彥宏,一定要指明是 "百度公司的李彥宏"。否則他會(huì)認(rèn)為百度是谷歌的一個(gè)部門,而李彥宏只是其中的一位員工而已。
這個(gè)例子展示了在命名空間下,使用限定類名和完全限定類名的區(qū)別。(完全限定類名 = 當(dāng)前命名空間 + 限定類名)
/* 導(dǎo)入命名空間 */ use 百度\李彥宏; new 李彥宏(); // 百度\李彥宏(實(shí)際結(jié)果) /* 設(shè)置別名 */ use 百度\李彥宏 AS CEO; new CEO(); // 百度\李彥宏(實(shí)際結(jié)果) /* 任何情況 */ new \百度\李彥宏();// 百度\李彥宏(實(shí)際結(jié)果)
↑ 第一種情況是別人已經(jīng)認(rèn)識(shí)李彥宏了,你只需要直接說(shuō)名字,他就能知道你指的是誰(shuí)。第二種情況是李彥宏就是他們的CEO,你直接說(shuō)CEO,他可以立刻反應(yīng)過(guò)來(lái)。
使用命名空間只是讓類名有了前綴,不容易發(fā)生沖突,系統(tǒng)仍然不會(huì)進(jìn)行自動(dòng)導(dǎo)入。
如果不引入文件,系統(tǒng)會(huì)在拋出 "Class Not Found" 錯(cuò)誤之前觸發(fā) spl_autoload_register 函數(shù),并將限定類名傳入作為參數(shù)。
所以上面的例子都是基于你已經(jīng)將相關(guān)文件手動(dòng)引入的情況下實(shí)現(xiàn)的,否則系統(tǒng)會(huì)拋出 " Class '百度\李彥宏' not found"。
使用 spl_autoload_register 實(shí)現(xiàn)文件自動(dòng)加載
接下來(lái)讓我們要在含有命名空間的情況下去實(shí)現(xiàn)自動(dòng)加載,如果此時(shí)你在用 __autoload() 函數(shù)來(lái)實(shí)現(xiàn)的話,是沒有效果的,可能是在命名空間下__autoload不能用吧,這里我們用 spl_autoload_register() 函數(shù)來(lái)實(shí)現(xiàn):
spl_autoload_register 函數(shù)的功能就是把傳入的函數(shù)(參數(shù)可以為回調(diào)函數(shù)或函數(shù)名稱形式)注冊(cè)到 SPL __autoload 函數(shù)隊(duì)列中,并移除系統(tǒng)默認(rèn)的 __autoload() 函數(shù)。
一旦調(diào)用 spl_autoload_register() 函數(shù),當(dāng)調(diào)用未定義類時(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 類,它使用 os 作為它的命名空間(建議文件名與類名保持一致):
namespace os; // 命名空間 class Linux // 類名 { function __construct() { echo '<h1>' . __CLASS__ . '</h1>'; } }
接著,在同一個(gè)目錄下新建一個(gè) PHP 文件,使用 spl_autoload_register 以函數(shù)回調(diào)的方式實(shí)現(xiàn)自動(dòng)加載:
spl_autoload_register(function ($class) { // class = os\Linux /* 限定類名路徑映射 */ $class_map = array( // 限定類名 => 文件路徑 'os\\Linux' => './Linux.php', ); /* 根據(jù)類名確定文件名 */ $file = $class_map[$class]; /* 引入相關(guān)文件 */ if (file_exists($file)) { include $file; } }); new \os\Linux();
這里我們使用了一個(gè)數(shù)組去保存類名與文件路徑的關(guān)系,這樣當(dāng)類名傳入時(shí),自動(dòng)加載器就知道該引入哪個(gè)文件去加載這個(gè)類了。
但是一旦文件多起來(lái)的話,映射數(shù)組會(huì)變得很長(zhǎng),這樣的話維護(hù)起來(lái)會(huì)相當(dāng)麻煩。如果命名能遵守統(tǒng)一的約定,就可以讓自動(dòng)加載器自動(dòng)解析判斷類文件所在的路徑。接下來(lái)要介紹的PSR-4 就是一種被廣泛采用的約定方式。
PSR-4(上篇文章講過(guò)) 是關(guān)于由文件路徑自動(dòng)載入對(duì)應(yīng)類的相關(guān)規(guī)范,規(guī)范規(guī)定了一個(gè)完全限定類名需要具有以下結(jié)構(gòu):
\<頂級(jí)命名空間>(\<子命名空間>)*\<類名>
如果繼續(xù)拿上面的例子打比方的話,頂級(jí)命名空間相當(dāng)于公司,子命名空間相當(dāng)于職位,類名相當(dāng)于人名。那么李彥宏標(biāo)準(zhǔn)的稱呼為 "百度公司 CEO 李彥宏"。
PSR-4 規(guī)范中必須要有一個(gè)頂級(jí)命名空間,它的意義在于表示某一個(gè)特殊的目錄(文件基目錄)。子命名空間代表的是類文件相對(duì)于文件基目錄的這一段路徑(相對(duì)路徑),類名則與文件名保持一致(注意大小寫的區(qū)別)。
舉個(gè)例子:在全限定類名 \app\view\news\Index 中,如果 app 代表 C:\Baidu,那么這個(gè)類的路徑則是 C:\Baidu\view\news\Index.php
我們就以解析 \app\view\news\Index 為例,編寫一個(gè)簡(jiǎn)單的 Demo:
$class = 'app\view\news\Index'; /* 頂級(jí)命名空間路徑映射 */ $vendor_map = array( 'app' => 'C:\Baidu', ); /* 解析類名為文件路徑 */ $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; // C:\Baidu\view\news\Index.php
通過(guò)這個(gè) Demo 可以看出限定類名轉(zhuǎn)換為路徑的過(guò)程。那么現(xiàn)在就讓我們用規(guī)范的面向?qū)ο蠓绞饺?shí)現(xiàn)自動(dòng)加載器吧。
首先我們創(chuàng)建一個(gè)文件 Index.php,它處于 \app\mvc\view\home 目錄中:
namespace app\mvc\view\home; class Index { function __construct() { echo '<h1> Welcome To YZMPHP </h1>'; } }
接著我們?cè)趧?chuàng)建一個(gè)加載類(不需要命名空間),它處于 \ 目錄中:
class Loader { /* 路徑映射 */ public static $vendorMap = array( 'app' => __DIR__ . DIRECTORY_SEPARATOR . 'app', ); /** * 自動(dòng)加載器 */ 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, '\\')); // 頂級(jí)命名空間 $vendorDir = self::$vendorMap[$vendor]; // 文件基目錄 $filePath = substr($class, strlen($vendor)) . '.php'; // 文件相對(duì)路徑 return strtr($vendorDir . $filePath, '\\', DIRECTORY_SEPARATOR); // 文件標(biāo)準(zhǔn)路徑 } /** * 引入文件 */ private static function includeFile($file) { if (is_file($file)) { include $file; } } }
最后,將 Loader 類中的 autoload 注冊(cè)到 spl_autoload_register 函數(shù)中:
include 'Loader.php'; // 引入加載器 spl_autoload_register('Loader::autoload'); // 注冊(cè)自動(dòng)加載 new \app\mvc\view\home\Index(); // 實(shí)例化未引用的類 /** * 輸出: <h1> Welcome To YZMPHP </h1> */
至此,PHP命名空間下的自動(dòng)加載的原理已經(jīng)全部講完了。
網(wǎng)友評(píng)論:
不錯(cuò)的文章
2019-01-02 18:30:12 回復(fù)