該類檔案在:ThinkPHP/Library/Think/Think.class.php
該類可以說是ThinkPHP架構最為核心的類庫,負責諸多配置載入,註冊核心系統擴充(自動載入類庫、異常處理、錯誤處理等),管理和維護類執行個體、別名映射,可以一說是一個架構的工廠(該類有些許物件導向弊端,比如:違背了物件導向單一職責,其負責功能複雜,關聯類別庫和檔案較多,有動一牽百的憂慮)。類中遇到的函數會在該類分析之後徹底分析,所涉及的其它類庫會專門講解。
一、類結構
namespace Think;//定義命名空間class Think { private static $_map = array();//類庫別名映射 private static $_instance = array();//儲存類執行個體(這麼說也不合理,等會分析該功能時具體說明) static public function start() {}//應用程式初始化 static public function addMap($class, $map=''){}// 註冊classmap static public function getMap($class=''){}// 擷取classmap public static function autoload($class) {}//類庫自動載入 static public function instance($class,$method='') {}//取得對象執行個體 支援調用類的靜態方法 static public function appException($e) {}//自訂異常處理 static public function appError($errno, $errstr, $errfile, $errline) {}//自訂錯誤處理 static public function fatalError() {} // 致命錯誤捕獲 static public function halt($error) {}//錯誤輸出 static public function trace($value='[think]',$label='',$level='DEBUG',$record=false) {}//添加和擷取頁面Trace記錄}
二、應用程式初始化start()方法分析,該方法包含一套錯誤和異常處理機制,非常受用。該方法作為ThinkPHP架構的引導介面,實現錯誤、異常處理,配置載入,別名映射,行為註冊,包含運行緩衝的產生,網站應用程式目錄檢測,自動類庫載入行為註冊。
/** * 應用程式初始化 * @access public * @return void */ static public function start() { //使用spl標準庫中提供__autoload()函數的預設實現,比__autoload()效率更高,更加靈活 //一下可以使用spl_autoload_register(array('Think\Think','autoload')); //建議使用spl_autoload_register(__NAMESPACE__.'\Think::autoload');實現 //一下所有註冊方式均可以使用上面3中形式傳遞參數 spl_autoload_register('Think\Think::autoload'); //註冊全域指令碼"解構函式",使用該方式註冊的函數,會在指令碼結束前調用,大多數情況用來處理致命錯誤 register_shutdown_function('Think\Think::fatalError'); //設定自訂錯誤處理函數,用於處理錯誤資訊 set_error_handler('Think\Think::appError'); //設定未異常處理函數 set_exception_handler('Think\Think::appException'); //可以把register_shutdown_function(),set_error_handler(),set_error_handler()3個函數組合完成自訂、多元化的錯誤處理模組 //根據STORAGE_TYPE的值設定分布式檔案儲存體方案,Storage是一個工廠類,用於管理和維護分布式檔案儲存體組件 //後面會詳細講解Storage類,並指出設計缺陷 Storage::connect(STORAGE_TYPE); //根據運行模式在運行緩衝目錄下產生編譯快取檔案APP_MODE.'~runtime.php',從而減少IO開銷 //下面會詳細介紹產生快取檔案的方式 $runtimefile = RUNTIME_PATH.APP_MODE.'~runtime.php'; //如果不是在偵錯模式,並且編譯快取檔案存在,直接載入編譯緩衝 if(!APP_DEBUG && Storage::has($runtimefile)){ Storage::load($runtimefile); }else{ //判斷編譯快取檔案是否存在,存在就刪除 if(Storage::has($runtimefile)) Storage::unlink($runtimefile); //先行編譯內容變數 $content = ''; //判斷是否存在運行模式設定檔,如果不存在就載入MODE_PATH.APP_MODE.'.php',運行模式設定檔會影響下列載入不同的類庫和配置 //回合組態檔案後期會詳細講解 $mode = include is_file(CONF_PATH.'core.php')?CONF_PATH.'core.php':MODE_PATH.APP_MODE.'.php'; //以下所有配置項載入都會根據載入的先後順序覆蓋之前的配置項,一般都是先載入ThinkPHP預設配置,再載入應用配置 //core下標決定要載入的核心類和函數檔案 foreach ($mode['core'] as $file){ if(is_file($file)) { include $file; //如果不是偵錯模式,則編譯該檔案內容並儲存到先行編譯內容變數中 if(!APP_DEBUG) $content .= compile($file); } } //config下標決定要載入的核心設定檔 foreach ($mode['config'] as $key=>$file){ //判斷下標是否為數字,如果不是就會把該設定檔中的配置項載入到對應的鍵下面,相當於給配置項增加一個緯度 is_numeric($key)?C(include $file):C($key,include $file); } //如果不是普通運行模式,則判斷是否存在運行模式應用設定檔 if('common' != APP_MODE && is_file(CONF_PATH.'config_'.APP_MODE.'.php')) C(include CONF_PATH.'config_'.APP_MODE.'.php'); //alias下標記錄類庫別名映射規則,ThinkPHP獨創別名機制,用於提升自動載入的效率 if(isset($mode['alias'])){ //由這句代碼可以看出alias規則可以是一個數組,或者將規則數組單獨作為一個檔案 self::addMap(is_array($mode['alias'])?$mode['alias']:include $mode['alias']); } //載入應用中定義的別名配置 if(is_file(CONF_PATH.'alias.php')) self::addMap(include CONF_PATH.'alias.php'); //tags下標用於標識系統行為,行為擴充具體由Hook鉤子類實現 if(isset($mode['tags'])) { //由這句代碼可以看出tags規則可以是一個數組,或者將規則數組單獨作為一個檔案 Hook::import(is_array($mode['tags'])?$mode['tags']:include $mode['tags']); } //載入應用中的行為擴充配置 if(is_file(CONF_PATH.'tags.php')) // 允許應用增加開發模式配置定義 Hook::import(include CONF_PATH.'tags.php'); //載入架構底層語言套件,有核心設定檔中的DEFAULT_LANG配置項決定 L(include THINK_PATH.'Lang/'.strtolower(C('DEFAULT_LANG')).'.php'); //如果不是偵錯模式則產生編譯快取檔案 if(!APP_DEBUG){ //namespace {}這種方式用於聲明代碼塊中的命名空間屬於全域命名空間 //這句代碼用於產生載入別名映射的php代碼 $content .= "\nnamespace { Think\Think::addMap(".var_export(self::$_map,true).");"; //L(".var_export(L(),true).");產生語言載入代碼 //C(".var_export(C(),true).');組建組態項載入代碼 //Think\Hook::import('.var_export(Hook::get(),true).');產生鉤子載入代碼 $content .= "\nL(".var_export(L(),true).");\nC(".var_export(C(),true).');Think\Hook::import('.var_export(Hook::get(),true).');}'; //將$content變數內容去除注釋和換行、空隔之後寫入到運行時編譯快取檔案 Storage::put($runtimefile,strip_whitespace('三、類庫別名映射機制實現addMap()和getMap()方法分析;該機制使用Think::_map變數儲存別名映射記錄,通過Think::addMap()添加或修改別名映射記錄,使用Think:getMap()擷取別名映射記錄。
/** * 註冊或修改類庫別名映射記錄 * @access public * @param class String|Array 如果為數組鍵為類庫別名,索引值為類庫實際位置;如果字串代表類庫別名 * @param map String 如果class為字串,該參數代表類庫實際位置,否則沒有意義 * @return void */ static public function addMap($class, $map=''){ //判斷class是否為數組,如果是就合并當前類庫別名記錄,如果有相同記錄就會覆蓋 if(is_array($class)){ self::$_map = array_merge(self::$_map, $class); }else{ //如果class不是數組,就作為別名儲存在類庫別名記錄中,並把map作為實際類庫位置 self::$_map[$class] = $map; } } /** * 根據別名擷取類庫實際地址 * @param class String 類庫別名 * @return Array|String|NULL如果class為空白則擷取所有類庫別名記錄,否者返回別名對應的類庫位置 */ static public function getMap($class=''){ //如果class為空白,直接返回所有類庫別名記錄 if(''===$class){ return self::$_map; //判斷對應別名是否在別名映射記錄中,如果存在返回類庫實際地址,否者返回null }elseif(isset(self::$_map[$class])){ return self::$_map[$class]; }else{ return null; } }四、ThinkPHP類庫自動載入機制autoload()方法分析,該方法是由Think::start()方法中的第一句代碼註冊實現spl_autoload_register('Think\Think::autoload');
/** * 類庫自動載入 * @param string $class 對象類名 * @return void */ public static function autoload($class) { //判斷是否存在別名映射 //標記一處bug,如果使用Think::addMap('Think\Test');註冊別名就完了,程式邏輯不嚴謹,不會有大的安全問題,可以無視 //具體可以看Think::addMap()方法的實現 if(isset(self::$_map[$class])) { include self::$_map[$class]; //建議在這裡renturn; //判斷是否存在\符號,存在則使用命名空間載入機制 //此處與配置說明不符'APP_AUTOLOAD_PATH' => '', // 自動載入的路徑 關閉APP_USE_NAMESPACE後有效 //現在判斷的是否在類名中使用命名規則而不是使用APP_USE_NAMESPACE配置,當然該項配置主要作用在路由模組,後續會講解 }elseif(strpos($class,'\\')){ //擷取命名空間的第一個命名範圍 $name = strstr($class, '\\', true); //判斷該命名空間的第一個命名範圍是否在Think約定範圍類,並在Library目錄下存在該目錄 if(in_array($name,array('Think','Org','Behavior','Com','Vendor')) || is_dir(LIB_PATH.$name)){ // Library目錄下面的命名空間自動定位 $path = LIB_PATH; }else{ //檢測自訂命名空間 否則就以模組為命名空間 $namespace = C('AUTOLOAD_NAMESPACE'); $path = isset($namespace[$name])? dirname($namespace[$name]).'/' : APP_PATH; } //這裡可以看出ThinkPHP命名空間的命名規則是以LIB_PATH和APP_PATH作為根目錄的目錄原則,也可以為AUTOLOAD_NAMESPACE配置項來自訂命名規則根目錄 $filename = $path . str_replace('\\', '/', $class) . EXT; //判斷檔案是否存在,為啥在註冊別名的時候不去判斷檔案是否存在呢?那樣是否可以無視一些有問題的別名映射,而使用正確的載入規則呢? if(is_file($filename)) { //如果在windows環境下運行,使用strpos來檢測是否大小寫一致問題,如果不一致直接返回 if (IS_WIN && false === strpos(str_replace('/', '\\', realpath($filename)), $class . EXT)){ return ; } //匯入類檔案 include $filename; //這裡建議return; } }else{ //不用按照設定檔中的APP_USE_NAMESPACE的值來決定是否APP_AUTOLOAD_LAYER配置該項 //只要在類庫載入中沒有使用命名空間就會調用以下規則來尋找類庫(要執行個體化的類不能聲明命名規則) foreach(explode(',',C('APP_AUTOLOAD_LAYER')) as $layer){ //判斷類名最後幾位是否符合APP_AUTOLOAD_LAYER配置項 if(substr($class,-strlen($layer))==$layer){ //載入當前模組下對應的類檔案,這個其實可以直接判斷檔案是否存在,並用include載入即可,沒有必要調用require_cache函數 //以上是個人見解,緣由是因為上面的載入機制都沒有使用,我覺得是否應該統一,而且自動載入類檔案,不用限制載入一次,如果已經載入了,也不會調用這個方法了。 if(require_cache(MODULE_PATH.$layer.'/'.$class.EXT)) { //負載檔案成功直接返回 return ; } } } //根據APP_AUTOLOAD_PATH配置設定的路徑規則自動搜尋並負載檔案 foreach (explode(',',C('APP_AUTOLOAD_PATH')) as $path){ //這裡同上,是否也可以不用調用import()方法載入,或者統一了呢? if(import($path.'.'.$class)) // 如果載入類成功則返回 return ; } } }五、管理類執行個體或者'緩衝'類靜態方法調用結果instance()方法分析,之前在類結構分析中說$_instance變數儲存類執行個體並不合理,因為該類還可以調用類的靜態方法,並'緩衝'結果。我在該方法注釋中提出一些個人見解,並不是說該方法設計的不合理,這個是畢竟是ThinkPHP專有方法,任何一個項目的設計都不會像我那般去考慮一個方法的諸多問題,首要問題是解決是否符合項目應用足矣。同樣看到這篇文章的朋友可以思考,在項目是否有很多類並不需要多個執行個體(沒有強制限制式的情況下),如果有那可以設計一個適合自己項目的偽單例工廠來管理這些類的執行個體。
/** * 取得對象執行個體 支援調用類的靜態方法 * 解析:我把該類看成一個偽單例工廠,不是嚴格要求類不許是單例,統一使用該方法擷取類對象,可以實現單例模式(極其適合php這種較為靈活的語言) * 說這是一個單例模式工廠不合格,因為沒有嚴格要求所管理類必須符合單例模式約束。 * 問題:該方法說此類可以調用類的靜態方法,並沒有約定靜態方法必須返回類的執行個體(self),可以返回任意結果,這個讓我很詫異 * 如果是想要緩衝類方法調用結果,是否應該提供給方法傳遞參數選項呢? * 如果僅僅為了管控類的執行個體(比如嚴格按照單例模式設計的類,如果要管理,必須使用靜態方法),是否應該說明或者在方法中檢測傳回值呢? * 解惑:這個畢竟不是提供給應用的方法(當然可以使用,設計初衷肯定不是,這算是ThinkPHPTeam Dev約定俗成的規定(口頭約束使用方法)吧?) * 這至少給我們一個啟示,可以這麼管理偽單例模式(口頭約束的方式,當然比之更可靠),之前我在一個項目中設計過這樣一個工廠類(那時候還沒有分析過任何產品的源碼) * @param string $class 對象類名 * @param string $method 類的靜態方法名 * @return object */ static public function instance($class,$method='') { //產生執行個體管理標識 $identify = $class.$method; //判斷是否已經存在改類執行個體標識 if(!isset(self::$_instance[$identify])) { //判斷類是否存在,這裡反應不能使用自動載入機制,必須在調用該方法前載入類檔案 if(class_exists($class)){ //執行個體化類,這裡可以看出並不能管理嚴格意義上的單例類 $o = new $class(); //判斷是否要調用靜態方法,並確定該類是否存在該方法 if(!empty($method) && method_exists($o,$method)) //返回調用結果(不明確是什麼) self::$_instance[$identify] = call_user_func(array(&$o, $method)); else //儲存類執行個體對象 self::$_instance[$identify] = $o; } else //輸出錯誤資訊 self::halt(L('_CLASS_NOT_EXIST_').':'.$class); } //返回執行個體對象 return self::$_instance[$identify]; }六、ThinkPHP內建錯誤處理和異常處理實現分析。appException()方法由Think::start()中set_exception_handler('Think\Think::appException');語句實現。appError()方法由Think::start()中set_error_handler('Think\Think::appError');語句實現,fatalError()方法由Think::start()中register_shutdown_function('Think\Think::fatalError');語句實現。halt()方法用來輸出重要錯誤資訊和異常,並終止程式執行。trace()方法用來記錄並管理Trace調試工具中的錯誤資訊。
/** * 自訂異常處理 * @access public * @param mixed $e 異常對象 * @param void */ static public function appException($e) { $error = array(); //擷取異常錯誤資訊 $error['message'] = $e->getMessage(); //擷取backtrace()回溯資訊 $trace = $e->getTrace(); //判斷是否由異常處理方法拋出,ThinkPHP自訂拋出異常處理函數 if('E'==$trace[0]['function']) { $error['file'] = $trace[0]['file'];//擷取錯誤檔案 $error['line'] = $trace[0]['line'];//擷取錯誤行號 }else{ $error['file'] = $e->getFile();//擷取錯誤檔案 $error['line'] = $e->getLine();//擷取錯誤行號 } //已格式錯誤回溯資訊 $error['trace'] = $e->getTraceAsString(); //寫入到錯誤記錄檔 Log::record($error['message'],Log::ERR); // 發送404資訊 header('HTTP/1.1 404 Not Found'); header('Status:404 Not Found'); //顯示錯誤資訊 self::halt($error); } /** * 自訂錯誤處理 * @access public * @param int $errno 錯誤類型 * @param string $errstr 錯誤資訊 * @param string $errfile 錯誤檔案 * @param int $errline 錯誤行數 * @return void */ static public function appError($errno, $errstr, $errfile, $errline) { switch ($errno) { //一些重要的錯誤資訊,會影響之後的程式執行 case E_ERROR: case E_PARSE: case E_CORE_ERROR: case E_COMPILE_ERROR: case E_USER_ERROR: //清空輸出緩衝(不知道在哪裡開啟了,後面分析會遇到) ob_end_clean();//其實就是把php預設輸出的錯誤資訊清除掉 //錯誤資訊 $errorStr = "$errstr ".$errfile." 第 $errline 行."; //根據LOG_RECORD是否記錄錯誤資訊,決定是否寫入錯誤記錄檔 if(C('LOG_RECORD')) Log::write("[$errno] ".$errorStr,Log::ERR); //輸出錯誤資訊 self::halt($errorStr); break; //可以忽略的錯誤資訊,不會輸出,會記錄到trace當中,使用SHOW_PAGE_TRACE配置可以查看的錯誤資訊 default: //這裡程式還要繼續執行,不能清空輸出緩衝,如果不希望顯示這類錯誤資訊,應當在php.ini中調節,或者使用ini_set()的函數改變 //錯誤資訊 $errorStr = "[$errno] $errstr ".$errfile." 第 $errline 行."; //記錄到trace當中 self::trace($errorStr,'','NOTIC'); break; } } // 致命錯誤捕獲 static public function fatalError() { //致命錯誤必須儲存到日誌中 Log::save(); //擷取上一個錯誤資訊,沒有錯誤就跳過了(⊙0⊙) if ($e = error_get_last()) { //處理致命錯誤資訊 switch($e['type']){ case E_ERROR: case E_PARSE: case E_CORE_ERROR: case E_COMPILE_ERROR: case E_USER_ERROR: //清空輸出緩衝,都導致程式停止了,還顯示什麼呀 ob_end_clean(); //輸出錯誤資訊 self::halt($e); break; } } } /** * 錯誤輸出 * @param mixed $error 錯誤 * @return void */ static public function halt($error) { $e = array(); //判斷是否是偵錯模式,或者命令列模式 if (APP_DEBUG || IS_CLI) { //偵錯模式下輸出錯誤資訊 //如果錯誤資訊不是一個數組,就回溯最後一次執行方法的資訊 if (!is_array($error)) { $trace = debug_backtrace(); $e['message'] = $error; $e['file'] = $trace[0]['file']; $e['line'] = $trace[0]['line']; ob_start();//開始輸出緩衝 debug_print_backtrace();//輸出一條回溯資訊 $e['trace'] = ob_get_clean();//擷取輸出緩衝資訊,並清空 } else { $e = $error; } if(IS_CLI){ //命令列模式,轉換為gbk編碼,終止程式執行並輸出錯誤資訊 exit(iconv('UTF-8','gbk',$e['message']).PHP_EOL.'FILE: '.$e['file'].'('.$e['line'].')'.PHP_EOL.$e['trace']); } } else { //不是偵錯模式,重新導向到錯誤頁面 $error_page = C('ERROR_PAGE');//擷取設定的錯誤頁面 if (!empty($error_page)) { //重新導向到錯誤頁面 redirect($error_page); } else { //根據SHOW_ERROR_MSG配置決定是否顯示詳細的錯誤資訊,還是採用ERROR_MESSAGE設定的錯誤資訊 $message = is_array($error) ? $error['message'] : $error; $e['message'] = C('SHOW_ERROR_MSG')? $message : C('ERROR_MESSAGE'); } } //根據TMPL_EXCEPTION_FILE配置決定調用錯誤資訊顯示模版,否者採用ThinkPHP預設模版 $exceptionFile = C('TMPL_EXCEPTION_FILE',null,THINK_PATH.'Tpl/think_exception.tpl'); include $exceptionFile; exit;//終止程式運行,很重要的。 } /** * 添加和擷取頁面Trace記錄 * @param string $value 變數 * @param string $label 標籤 * @param string $level 記錄層級(或者頁面Trace的選項卡) * @param boolean $record 是否記錄日誌 * @return void */ static public function trace($value='[think]',$label='',$level='DEBUG',$record=false) { //採用靜態變數儲存Trace記錄 static $_trace = array(); if('[think]' === $value){ // 擷取trace資訊 return $_trace; }else{ //錯誤資訊 $info = ($label?$label.':':'').print_r($value,true); $level = strtoupper($level);//將錯誤層級轉換為大寫 //如果是AjAX請求或者不顯示TRACE調試工具,或者$record要求記錄日誌,就不會記錄該條錯誤資訊 if((defined('IS_AJAX') && IS_AJAX) || !C('SHOW_PAGE_TRACE') || $record) { Log::record($info,$level,$record);//將錯誤資訊寫入到記錄檔 }else{ //判斷錯誤等級是否存在或者該類錯誤資訊是否達到錯誤類型記錄上限,由TRACE_MAX_RECORD配置 if(!isset($_trace[$level]) || count($_trace[$level])>C('TRACE_MAX_RECORD')) { //這裡有個我很詫異的地方,當錯誤類別記錄達到錯誤類型記錄上限的是否為什麼要重設該類型錯誤記錄 //而不是不記錄當前錯誤資訊,或者刪除最先一條的錯誤資訊,追加到最後 //我建議是不理會比較合理,因為調試錯誤,也有先後嗎,先把之前遇到的錯誤解決,就會看到新的錯誤了(這是否有點坑⊙0⊙) $_trace[$level] = array(); } //按錯類別記錄錯誤資訊 $_trace[$level][] = $info; } } }七、總結:對該類分析,主要掌控php錯誤處理和異常處理方面的知識,並瞭解基於命名空間自動載入的規則定義基礎,同樣接觸了ThinkPHP運行時編譯緩衝機制帶來的IO最佳化思路以及類庫別名機制對與類自動載入帶來的最佳化。在分析該類時站在ThinkPHP應用外對該類提出幾處質疑,僅為個人對物件導向設計的理解和認知,不作為詳細參考。
http://www.bkjia.com/PHPjc/755775.htmlwww.bkjia.comtruehttp://www.bkjia.com/PHPjc/755775.htmlTechArticle該類檔案在:ThinkPHP/Library/Think/Think.class.php 該類可以說是ThinkPHP架構最為核心的類庫,負責諸多配置載入,註冊核心系統擴充(自動載入類...