當我最開始寫php的時候,總是擔心這個問題:我在這兒new的一個class能加載到對應的類文件嗎?畢竟一運行就報Fatal Error,什麼**文件沒找到,類無法實例化等等是一種很“低級”的錯誤,怕別人看了笑話。於是每接一個新任務,我總想把它的加載過程弄清楚(以前只知道幾個html標簽和樣式,不知算不算web開發),有時頭兒看了說還有閒心看這個,趕緊寫邏輯,照這樣做就行了......你妹你知道當然有把握了D:,後來發現原來流程都差不多。
在一個IDE中開發時,如C++/Java,一般是新建一個工程,通過IDE新添加一個文件到指定目錄下,然後#include/Import進來即可,php則使這一步驟更加過程化,文件的加載過程基本確定了這個project(框架或者自搭的項目)的目錄結構和文件的分門別類。
不管框架還是自搭的項目總得有個入口文件,這時要事先加載一些基本信息,如配置文件、通用方法等,使用的基本是手動直接加載單個文件形式,使用下面四個方法之一:
include、require、include_once、require_once
include('config.php'); require('database.php');
涉及到類文件的加載,少部分是直接加載,比如,通用方法作為靜態方法寫在一個類Utilities中,因為是後邊很多要用到的方法(如錯誤輸出、curl請求、隨機字符串生成...),所以用類封裝起來,一般也是在加載配置文件時連帶加載進來
include('Utilities.php');
而更通用的情況是:類的動態加載。首先不談的加載的方式,來看看大概什麼時候會用到一個類和實例:
1. 最明顯的,$obj = new A; 它的變種$className = 'A'; $obj = $className; 都一樣;
2. 類的靜態方法、靜態變量和常量的調用,即Utilities::httpRequest()、Utilities::$instance、Utilities::HOST;
3. 在php函數中,使用了回調函數的情況,最典型的call_user_func_array()(call_user_func),還有其他用到了callback的地方,如數組中的array_walk、array_map,它們需要一個回調函數作為參數。
回調函數非常靈活,不止可以是簡單函數,還可以是對象的方法,包括靜態類方法。因為可以用對象方法或靜態方法,所以這鐘時候也要去加載對應的類文件。自php5.3起,回調函數還可以像js中,用匿名函數來實現。
class A{ public static function cube($var){ return pow($var, 3); } public function twice($var){ return 2*$var; } } // 使用類的靜態方法 $num = call_user_func('A::cube', 5); // 使用對象 $obj = new A; $num = call_user_func_array(array($obj, 'twice'), array(7));
嚴格來說上例中的call_user_func_array在之前已經實例化了對象,但是存在這麼個用法,它完全也可以使用類靜態方法。
首先要明白的是,為什麼需要動態加載。php是腳本語言,我們訪問時,是以腳本為可用資源,比如現在根目錄有個index.php文件,它沒有include任何其他文件,當我們直接以localhost/index.php來訪問時,可以訪問到index.php中的全部資源,如果index.php中定義了一個普通類A,在該腳本中實例化一個A的對象時,程序會這樣反應:哦,我已經看到了A的定義,可以直接實例化它(不需要加載其他文件)。如果還有類B、C、D等很多類,全部寫在index.php中顯然不行,那就寫在其他文件中,再include進來(include已經在做加載的工作了),這樣對程序來說,也是“可見”的了。
但是隨著系統功能的增多,類越來越多,各個類的功能也不同,有的直接定義數據庫的操作,讀取數據庫的數據,有的是控制訪問腳本時要運行的方法,有的則是將要展現出來的頁面,有的是我們引用的第三方核心庫,於是,當我們把所有的文件放在一個目錄中時,雖然可以直接include加載,但這些文件擺放顯得既雜亂無章又難找,維護成本還高。好呗,那就在根目錄下再分別建幾個目錄,目錄A專門存放與數據庫打交道的腳本,目錄B是系統的各種配置信息文件,目錄C是控制我們進入程序時的入口控制方法的腳本,目錄D是即將展示到浏覽器的頁面......
於是MVC架構慢慢就演化出來了,我們不能再像以前那樣直接include,腳本都放在特定的目錄下,如Controller目錄下存放的是各種控制器,加載控制器時,我們得這樣include('root/Controller/indexController.php'),每次都在文件前面弄一大串的include不僅看著頭疼,簡直讓人累覺不愛。既然有了獲取當前文件路徑和類名的現成方法,為何不將類名與文件名對應起來,而只要是控制器類的腳本就全放在根目錄的Controller子目錄下邊,就可以寫一個方法,只要是控制器類,在這個方法中運行include(ROOT.'Controller/'.$className.'.php');這一句,ROOT為根目錄常量,$className為傳入的類名,只要是模型類,就這樣include(ROOT.'Model/'.$className.'.php');,全憑這個函數來動態控制到哪個目錄裡邊去找,這個project可能就是這樣的:
無形中,就建立起了類名和文件名的對應規則,文件和所在的目錄的對應規則,該project下有哪些這樣的目錄和文件呢?啊原來是放控制器的Controller、放配置信息的Config等等,再次於無形中得知了這個project的結構,而上面說的,利用函數根據一定條件(傳入參數)可知自動到哪個目錄下去加載該文件,而不是一個個寫死的include,就是所謂的文件的動態加載了。
因此,當你要新建一個**類文件時,也就知道,哦在這個project中,我應該放在這個目錄下,文件的命名應該與類名相同,這樣就一定能加載到了~~~接下來就是寫業務邏輯的一個“愉快的過程”。
知道什麼時候會動態加載及為什麼要動態加載後,接下來就是來實現了,也就是上面說到的利用函數來加載某個文件,就是要寫好這個“函數”來實現這個過程。常用的有三種方式:
1. __autoload
我第一次學的時候就是用的就是這個,魔術函數,只要定義了php程序就會在要用到一個類時自動調用它進行文件動態加載,一樣,既然它是個函數,就要讓程序對__autoload的定義可見,不然從哪兒調用它呢?一般來說,作為後邊程序大部分地方要用到的方法,我們都會放在一個單獨的文件中,在程序的入口處加載進來,一個project總得有幾個文件是手動include的,完全可以在開頭單獨include進來,或者放在配置信息中,加載配置信息時就加載進來了。它的原型:
void __autoload ( string $class )
參數當前加載的類名名稱(注意如果有命名空間,則包含命名空間前綴),下面是一個針對上面的圖片結構的簡單示例:
// file: autoload.php // ROOT為已經定義的根目錄常量 function __autoload($className){ try{ if(file_exists(ROOT.'Controller/'.$className.'.php')){// 檢查Controller include(ROOT.'Controller/'.$className.'.php'); } else if(file_exists(ROOT.'Model/'.$className.'.php')){// 檢查Model include(ROOT.'Model/'.$className.'.php'); } else if(file_exists(ROOT.'Lib/'.$className.'.php')){// 檢查Lib include(ROOT.'Lib/'.$className.'.php'); } else{ // 找不到該文件 throw new Exception("ERROR: can't find file {$className}.php"); } } catch(Exception $e){ echo $e.getMessage(); exit; } }
2. spl_autoload_register
__autoload實際上也差不多了,但它是php定義的,如果現在有個東西寫了並調用之後,就告訴程序說,我不用__autoload來加載文件了,我已經定義了一個專門加載文件的方法(比如名稱是loadClass),以後需要加載一個類文件時,你就用它吧。spl_autoload_register就是這樣一個能告訴程序這樣去做的方法,而且自定義加載方法將會更靈活,可以指定多個加載函數,spl_autoload_register函數會將這些函數放在一個隊列中,並激活它們,在調用時逐個激活:“If there must be multiple autoload functions, spl_autoload_register() allows for this. It effectively creates a queue of autoload functions, and runs through each of them in the order they are defined. ”,php.net上(http://php.net/manual/en/function.spl-autoload-register.php)也確實如此解釋,spl_autoload_unregister則是從加載函數隊列中注銷。
另外spl_autoload_functions()函數,可以獲取我們注冊了哪些函數;spl_autoload_call($class)函數,嘗試調用所有已注冊的加載函數來加載$class的類文件。
對於spl_autoload_register的解釋,我的理解是,如果用spl_autoload_register注冊了n個函數在加載隊列中,因為它自動激活它們嘛,現在我要實例化一個類,在第1個加載函數中加載失敗了,然後嘗試第2個函數,第二個失敗則嘗試第3個,''',直到第n個函數走完,若還沒加載成功,就報錯,只要中間一個加載成功就成功了,but事實好像有點出入。
還是用上一個圖片中的目錄結構,
1、在Controller目下創建indexController.php文件,包含類indexController;
2、在Model目錄下創建userModel.php文件,包含類userModel;
3、首頁寫個類加載腳本Autoload.php,代碼如下:
// file: Autoload.php define('DS', DIRECTORY_SEPARATOR); define('ROOT', rtrim(dirname(__FILE__), '/\\').DS); class Autoload{ public static function autoloadRegister($loadFunc = 'Autoload::loadControllerClass', $enable = true){ return $enable ? spl_autoload_register($loadFunc) : spl_autoload_unregister($loadFunc); } // 加載控制器類 public static function loadControllerClass($className){ if(file_exists(ROOT.'Controller'.DS.$className.'.php')){// 檢查Controller include(ROOT.'Controller'.DS.$className.'.php'); echo ROOT.'Controller'.DS.$className.'.php'.'<br/>'; } else{ echo "ERROR: can't find file {$className}.php in ".ROOT."Controller"; exit; } } // 加載模型類 public static function loadModelClass($className){ if(file_exists(ROOT.'Model'.DS.$className.'.php')){// 檢查Model include(ROOT.'Model'.DS.$className.'.php'); echo ROOT.'Model'.DS.$className.'.php'.'<br/>'; } else{ echo "ERROR: can't find file {$className}.php in ".ROOT."Model"; exit; } } }
4、測試腳本,測試類是否能加載
// 注冊兩個加載函數 Autoload::autoloadRegister('Autoload::loadControllerClass'); Autoload::autoloadRegister('Autoload::loadModelClass'); // 查看總共注冊了哪些加載函數 echo 'register functions=> <pre>'; print_r(spl_autoload_functions()); // 分別實例化一個Controller類和Model類 $indexCon = new indexController; $userMod = new userModel;
結果是這樣
這不科學啊,spl_autoload_functions數組顯示兩個函數都注冊了,但是當實例化userModel類時它還是跑到Controller目錄中去找,兩個類的實例化調用的自動加載方法都是Autoload::loadControllerClass,所以userModel類文件加載報錯......注意到spl_autoload_register方法的第三個參數, 是添加一個加載函數時放在棧中的位置,於是我另寫一個類似的類otherLoad,只是為了將loadModelClass方法放到隊列首部:
class otherLoad{ public static function autoloadRegister($loadFunc = 'otherLoad::loadModelClass', $enable = true){ // 默認將loadModelClass放在隊首 return $enable ? spl_autoload_register($loadFunc, true, true) : spl_autoload_unregister($loadFunc); } // 加載模型類 public static function loadModelClass($className){ if(file_exists(ROOT.'Model'.DS.$className.'.php')){// 檢查Model include(ROOT.'Model'.DS.$className.'.php'); echo ROOT.'Model'.DS.$className.'.php'.'<br/>'; } else{ echo "ERROR: can't find file {$className}.php in ".ROOT."Model"; exit; } } }
測試是這樣
// 注冊三個加載函數 Autoload::autoloadRegister('Autoload::loadControllerClass'); Autoload::autoloadRegister('Autoload::loadModelClass'); otherLoad::autoloadRegister('otherLoad::loadModelClass'); // 查看總共注冊了哪些加載函數 echo 'register functions=> <pre>'; print_r(spl_autoload_functions()); // 分別實例化一個Controller類和Model類 $indexCon = new indexController; $userMod = new userModel;
這次的結果是這樣:
可以看到,這次是在加載indexController類時不成功,因為它只調用了loadModelClass方法,再看看spl_autoload_functions返回的數組,otherLoad類的loadModelClass方法在最前面,難道說,只有在加載函數隊列最前面的函數才被用於自動加載,其他無效?這是什麼狀況?
使用spl_autoload_call('indexController')來“嘗試調用所有已注冊的函數來裝載請求類”,還是報這個錯。
翻了下別人的文章,包括github上的博客,也就是列舉了下手冊上說的“可以一次注冊多個加載函數 bala bala......”,難道沒有人試過,還是我的理解有問題>3<...(win下測試,php版本5.4.10)真是這樣的話spl_autoload_register方法就沒多大意義嘛╮(╯▽╰)╭......
關於spl_autoload_register還有幾個有意思的地方:
1、 一個函數只會加載到函數隊列中一次,重復加載也是如此;
2、 spl_autoload_register如果不指定加載函數(第一個參數),則默認使用加載函數spl_autoload(功能類似於__autoload,是它的默認實現形式)
3、 spl_autoload_register指定了__autoload為加載函數,則一定要實現__autoload;
4、 同時實現了spl_autoload_register和__autoload,優先使用spl_autoload_register注冊的加載函數。
以上幾種情況幾乎都可從php.net的note中找到測試例子,老外寫得挺有意思,可供參考。上面第2點還需要注意,比如現在在根目錄創建一個目錄,使用默認函數來加載:
// 設置加載文件的擴展名,將只加載*.php的文件 spl_autoload_extensions('.php'); // 默認使用spl_autoload加載文件,只能加載當前目錄下文件:小寫類名.php spl_autoload_register(); // 測試 // $obj = new A;
spl_autoload_extensions設置加載時只認哪些擴展類型的文件,默認是.php或者.inc文件,這裡設置成.php,然後就是調用注冊函數。在根目錄下創建一個A.php文件,新建一個類A,加載成功,再將文件名改成a.php,照樣加載成功。需要留意spl_autoload默認將類名轉小寫,但是A.php照樣加載成功,因為Windows的文件是大小寫不敏感的(在同一目錄下創建一個d.txt,再創建D.txt會認為是同一個文件),對於Mac OS X也是這樣,但Linux就是大小寫敏感了,測試時要注意這點。
也不是全要自動加載,如CI,它將加載文件封裝為一個核心類CI_Loader,程序啟動時先include必要的腳本(其他要用的核心類),然後再等需要使用時,CI_Loader實例作為當前控制器類或模型類等的一個屬性成員,通過調用它的方法來include各種model(模型)、view(視圖)、database(數據庫對象)、helper(輔助函數)等等。
無論用不用動態加載,必須保證的是,文件分門別類的放好,文件按一定規則命名,這是一個健壯、高擴展、高易用的project必備的,寫起代碼來也方便。當然加載文件的多少,占內存的多少,各有不同,也是評判一個框架的若干標准。弄清楚加載方式,熟悉一個框架結構不就是很容易的事了=_=...