從本篇開始,將深入CI框架的內部,一步步去探索這個框架的實現、結構和設計。
Common.php文件定義了一系列的全局函數(一般來說,全局函數具有最高的加載優先權,因此大多數的框架中BootStrap引導文件都會最先引入全局函數,以便於之后的處理工作)。
打開Common.php中,第一行代碼就非常詭異:
if ( ! defined('BASEPATH')) exit('No direct script access allowed');
上一篇(CI框架源碼閱讀筆記2 一切的入口 index.php)中,我們已經知道,BASEPATH是在入口文件中定義的常量。這里做這個判斷的原因是:避免直接訪問文件,而必須由index.php入口文件進入。其實不僅是Common.php,System中所有文件,幾乎都要引入這個常量的判斷,避免直接的腳本訪問:
本文件中定義的函數如下(查看方式 print_r(get_defined_functions())):
CI中所有全局函數的定義方式都為:
if ( ! function_exists('func_name')){ function func_name(){ //function body } }
這樣做,是為了防止定義重名函數(之后如果我們要定義系統的全局函數,也都將使用這種定義方式)。下面,一個個展開來看:
1. is_php
這個函數的命名很明顯,就是判斷當前環境的PHP版本是否是特定的PHP版本(或者高於該版本)
該函數內部有一個static的$_is_php數組變量,用於緩存結果(因為在特定的運行環境中,PHP的版本是已知的且是不變的,所以通過緩存的方式,避免每次調用時都去進行version_compare。這種方式,與一般的分布式緩存(如Redis)的處理思維是一致的,不同的是,這里是使用static數組的方式,而分布式緩存大多使用內存緩存)。
為什么要定義這個函數呢?這是因為,CI框架中有一些配置依賴於PHP的版本和行為(如magic_quotes,PHP 5.3版本之前,該特性用於指定是否開啟轉義,而PHP5.3之后,該特性已經被廢棄)。這就好比是針對不同的瀏覽器進行Css Hack一樣(這里僅僅是比喻,實際上,PHP並沒有太多的兼容性問題)。
具體的實現源碼:
function is_php($version = '5.0.0') { static $_is_php; $version = (string)$version; if ( ! isset($_is_php[$version])) { $_is_php[$version] = (version_compare(PHP_VERSION, $version) < 0) ? FALSE : TRUE; } return $_is_php[$version]; }
2. is_really_writable
這個函數用於判斷文件或者目錄是否真實可寫,一般情況下,通過內置函數is_writable()返回的結果是比較可靠的,但是也有一些例外,比如:
(a). Windows中,如果對文件或者目錄設置了只讀屬性,則is_writable返回結果是true,但是卻無法寫入。
(b). Linux系統中,如果開啟了Safe Mode,則也會影響is_writable的結果
因此,本函數的處理是:
如果是一般的Linux系統且沒有開啟safe mode,則直接調用is_writable
否則:
如果是目錄,則嘗試在目錄中創建一個文件來檢查目錄是否可寫
如果是文件,則嘗試以寫入模式打開文件,如果無法打開,則返回false
注意,即使是使用fopen檢查文件是否可寫,也一定記得調用fclose關閉句柄,這是一個好的習慣。
該函數的源碼:
function is_really_writable($file) { // If we're on a Unix server with safe_mode off we call is_writable if (DIRECTORY_SEPARATOR == '/' AND @ini_get("safe_mode") == FALSE) { return is_writable($file); } // For windows servers and safe_mode "on" installations we'll actually write a file then read it if (is_dir($file)) { $file = rtrim($file, '/').'/'.md5(mt_rand(1,100).mt_rand(1,100)); if (($fp = @fopen($file, FOPEN_WRITE_CREATE)) === FALSE) { return FALSE; } fclose($fp); @chmod($file, DIR_WRITE_MODE); @unlink($file); return TRUE; } elseif ( ! is_file($file) OR ($fp = @fopen($file, FOPEN_WRITE_CREATE)) === FALSE) { return FALSE; } fclose($fp); return TRUE; }
3. load_class
這個函數有幾個特殊的地方需要重點關注:
(1). 注意這個函數的簽名,function &load_class( $class,$directory,$prefix).看到前面那個特殊的&符號沒?沒錯,這個函數返回的是一個class實例的引用. 對該實例的任何改變,都會影響下一次函數調用的結果。
(2). 這個函數也有一個內部的static變量緩存已經加載的類的實例,實現方式類似於單例模式(Singleton)
(3). 函數優先查找APPPATH和BASEPATH中查找類,然后才從$directory中查找類,這意味着,如果directory中存在着同名的類(指除去前綴之后同名),CI加載的實際上是該擴展類。這也意味着,可以對CI的核心進行修改或者擴展。
下面是該函數的源碼:
function &load_class($class, $directory = 'libraries', $prefix = 'CI_') { /* 緩存加載類的實例 */ static $_classes = array(); if (isset($_classes[$class])) { return $_classes[$class]; } $name = FALSE; /* 先查找系統目錄 */ foreach (array(APPPATH, BASEPATH) as $path) { if (file_exists($path.$directory.'/'.$class.'.php')) { $name = $prefix.$class; if (class_exists($name) === FALSE) { require($path.$directory.'/'.$class.'.php'); } break; } } /* 查找之后並沒有立即實例化,而是接着查找擴展目錄 */ if (file_exists(APPPATH.$directory.'/'.config_item('subclass_prefix').$class.'.php')) { $name = config_item('subclass_prefix').$class; if (class_exists($name) === FALSE) { require(APPPATH.$directory.'/'.config_item('subclass_prefix').$class.'.php'); } } /* 沒有找到任何文件 */ if ($name === FALSE) { exit('Unable to locate the specified class: '.$class.'.php'); } /* 將$class計入已加載的類列表 */ is_loaded($class); /* 取得實例化 */ $_classes[$class] = new $name(); return $_classes[$class]; }
4. is_loaded
這個函數用於追蹤所有已加載的class。代碼比較簡潔,沒有太多可講的地方,這里直接貼出源碼:
function &is_loaded($class = '') { static $_is_loaded = array(); if ($class != '') { $_is_loaded[strtolower($class)] = $class; } return $_is_loaded; }
5. get_config
這個函數用於加載主配置文件(即位於config/目錄下的config.php文件,如果定義了針對特定ENVIRONMENT的config.php文件,則是該文件)。該函數的簽名為:
function &get_config($replace = array())
有幾個需要注意的點:
(1). 函數只加載主配置文件,而不會加載其他配置文件(這意味着,如果你添加了其他的配置文件,在框架預備完畢之前,不會讀取你的配置文件)。在Config組件實例化之前,所有讀取主配置文件的工作都由該函數完成。
(2). 該函數支持動態運行的過程中修改Config.php中的條目(配置信息只可能修改一次,因為該函數也有static變量做緩存,若緩存存在,則直接返回配置)
(3). Return $_config[0] = & $config。是config文件中$config的引用,防止改變Config的配置之后,由於該函數的緩存原因,無法讀取最新的配置。
這里還有一點無法理解,作者使用了$_config數組來緩存config,而只使用了$_config[0],那么問題來了,為什么不用單一變量代替,即:$_config = & $config; 如果有知道原因的童鞋,麻煩告知一聲。
該函數的實現源碼:
function &get_config($replace = array()) { static $_config; if (isset($_config)) { return $_config[0]; } if ( ! defined('ENVIRONMENT') OR ! file_exists($file_path = APPPATH.'config/'.ENVIRONMENT.'/config.php')) { $file_path = APPPATH.'config/config.php'; } if ( ! file_exists($file_path)) { exit('The configuration file does not exist.'); } require($file_path); if ( ! isset($config) OR ! is_array($config)) { exit('Your config file does not appear to be formatted correctly.'); } if (count($replace) > 0) { foreach ($replace as $key => $val) { if (isset($config[$key])) { $config[$key] = $val; } } } return $_config[0] =& $config; }
6. config_item
這個函數調用了load_config,並獲取相應的設置條目。代碼比較簡潔。不做過多的解釋,同樣只貼出源碼:
function config_item($item) { static $_config_item = array(); if ( ! isset($_config_item[$item])) { $config =& get_config(); if ( ! isset($config[$item])) { return FALSE; } $_config_item[$item] = $config[$item]; } return $_config_item[$item]; }
7. show_error
這是CI定義的可以用來展示錯誤信息的函數,該函數使用了Exceptions組件(之后我們將看到,CI中都是通過Exceptions組件來管理錯誤的)來處理錯誤。
例如,我們可以在自己的應用程序控制器中調用該函數展示錯誤信息:
Show_error(“trigger error info”);
CI框架的錯誤輸出還算是比較美觀:
注意該函數不僅僅是顯示錯誤,而且會終止代碼的執行(exit)
該函數的源碼:
function show_error($message, $status_code = 500, $heading = 'An Error Was Encountered') { $_error =& load_class('Exceptions', 'core'); echo $_error->show_error($heading, $message, 'error_general', $status_code); exit; }
8. show_404
沒有太多解釋的東西,返回404頁面
源碼:
function show_404($page = '', $log_error = TRUE) { $_error =& load_class('Exceptions', 'core'); $_error->show_404($page, $log_error); exit; }
9. log_message
調用Log組件記錄log信息,類似Debug。需要注意的是,如果主配置文件中log_threshold被設置為0,則不會記錄任何Log信息,該函數的源碼:
function log_message($level = 'error', $message, $php_error = FALSE) { static $_log; if (config_item('log_threshold') == 0) { return; } $_log =& load_class('Log'); $_log->write_log($level, $message, $php_error); }
10. set_status_header
CI框架允許你設置HTTP協議的頭信息(具體的HTTP狀態碼和對應含義可以參考:http://blog.csdn.net/ohmygirl/article/details/6922313)。設置方法為:
$this->output->set_status_header(“401”,“lalalala”);(CI的Output組件暴露了set_status_header()對外接口,該接口即是調用set_status_header函數)
值得注意的是,現在很多服務器內部擴展加入了自定義的狀態碼,如nginx:
ngx_string(ngx_http_error_495_page), /* 495, https certificate error */ ngx_string(ngx_http_error_496_page), /* 496, https no certificate */ ngx_string(ngx_http_error_497_page), /* 497, http to https */ ngx_string(ngx_http_error_404_page), /* 498, canceled */ ngx_null_string, /* 499, client has closed connection */
所以你在查看服務器的error_log時,如果看到了比較詭異的錯誤狀態碼,不要驚慌,這不是bug. 這也說明,如果你要自定義自己的狀態碼和狀態碼描述文案,可以在該函數的內部$stati變量中添加自定義的狀態碼和文案。更多詳細的內容,可以查看header函數的manual。
源碼:
function set_status_header($code = 200, $text = '') { /* 所有的已定義狀態碼和描述文本 */ $stati = array(
/* 2xx 成功 */ 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', /* 3xx 重定向 */ 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 304 => 'Not Modified', 305 => 'Use Proxy', 307 => 'Temporary Redirect', /* 4xx 客戶端錯誤 */ 400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Timeout', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Long', 415 => 'Unsupported Media Type', 416 => 'Requested Range Not Satisfiable', 417 => 'Expectation Failed', /* 5xx 服務器端錯誤 */ 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported' ); /* 狀態碼為空或者不是數字,直接拋出錯誤並退出 */ if ($code == '' OR ! is_numeric($code)) { show_error('Status codes must be numeric', 500); } if (isset($stati[$code]) AND $text == '') { $text = $stati[$code]; } /* 設置的狀態碼不在已定義的數組中 */ if ($text == '') { show_error('No status text available. Please check your status code number or supply your own message text.', 500); } $server_protocol = (isset($_SERVER['SERVER_PROTOCOL'])) ? $_SERVER['SERVER_PROTOCOL'] : FALSE; /* PHP以CGI模式運行 */ if (substr(php_sapi_name(), 0, 3) == 'cgi') { header("Status: {$code} {$text}", TRUE); } elseif ($server_protocol == 'HTTP/1.1' OR $server_protocol == 'HTTP/1.0')/* 檢查HTTP協議 */ { header($server_protocol." {$code} {$text}", TRUE, $code); } else { header("HTTP/1.1 {$code} {$text}", TRUE, $code);/* 默認為HTTP/1.1 */ } }
11. _exception_handler
先看函數的簽名:
function _exception_handler($severity, $message, $filepath, $line);
$ severity :發生錯誤的錯誤碼。整數
$message :錯誤信息。
$filepath :發生錯誤的文件
$line :錯誤的行號
這個函數會根據當前設置的error_reporting的設置和配置文件中threshold的設置來決定PHP錯誤的顯示和記錄。在CI中,這個函數是作為set_error_handler的callback, 來代理和攔截PHP的錯誤信息(PHP手冊中明確指出:以下級別的錯誤不能由用戶定義的函數來處理: E_ERROR
、 E_PARSE
、 E_CORE_ERROR
、 E_CORE_WARNING
、 E_COMPILE_ERROR
、 E_COMPILE_WARNING
,和在 調用 set_error_handler() 函數所在文件中產生的大多數 E_STRICT
。同樣,如果在set_error_handler調用之前發生的錯誤,也無法被_exception_handler捕獲,因為在這之前,_exception_handler尚未注冊)。
再看源碼實現:
if ($severity == E_STRICT){ return; }
E_STRICT是PHP5中定義的錯誤級別,是嚴格語法模式的錯誤級別,並不包含在E_STRICT. 由於E_STRICT級別的錯誤可能會很多,因此,CI的做法是,忽略這類錯誤。
函數中實際處理和記錄錯誤信息的是Exception組件:
$_error =& load_class('Exceptions', 'core');
然后根據當前的error_reporting設置,決定是顯示錯誤(show_php_error)還是記錄錯誤日志(log_exception):
if (($severity & error_reporting()) == $severity) { $_error->show_php_error($severity, $message, $filepath, $line); }
注意,這里是位運算&而不是邏輯運算&&, 由於PHP中定義的錯誤常量都是整數,而且是2的整數冪(如
1 E_ERROR
2 E_WARNING
4 E_PARSE
8 E_NOTICE
16 E_CORE_ERROR
...
),因此可以用&方便判斷指定的錯誤級別是否被設置,而在設置的時候,可以通過|運算:
/* 顯示E_ERROR,E_WARNING,E_PARSE錯誤 */ error_reporting(E_ERROR | E_WARNING | E_PARSE); /* 顯示除了E_NOTICE之外的錯誤 */ error_reporting(E_ALL & ~E_NOTICE | E_STRICE);
這與Linux的權限設置rwx的設計思想是一致的(r:4 w:2 x:1)
有時候僅僅顯示錯誤是不夠的,還需要記錄錯誤信息到文件:
如果主配置文件config.php中$config['log_threshold'] == 0,則不記錄到文件:
if (config_item('log_threshold') == 0) { return; }
否者,記錄錯誤信息到文件(這之中,調用組件Exception去寫文件,Exception組件中會調用log_message函數,最終通過Log組件記錄錯誤信息到文件。模塊化的一個最大特點是每個組件都負責專門的職責,而模塊可能還會暴露接口被其他組件調用。)
最后,貼上完整的源碼:
function _exception_handler($severity, $message, $filepath, $line) { if ($severity == E_STRICT) { return; } $_error =& load_class('Exceptions', 'core'); if (($severity & error_reporting()) == $severity) { $_error->show_php_error($severity, $message, $filepath, $line); } if (config_item('log_threshold') == 0) { return; } $_error->log_exception($severity, $message, $filepath, $line); }
12. Remove_invisiable_character
這個函數的含義非常明確,就是去除字符串中的不可見字符。這些不可見字符包括:
ASCII碼表中的00-31,127(保留09,10,13,分別為tab,換行和回車換行,這些雖然不可見,但卻是格式控制字符)。然后通過正則替換去除不可見字符:
do{ $str = preg_replace($non_displayables, '', $str, -1, $count); } while ($count);
理論上將,preg_replace會替換所有的滿足正則表達式的部分,這里使用while循環的理由是:可以去除嵌套的不可見字符。如 %%0b0c。如果只執行一次替換的話,剩余的部分%0c依然是不可見字符,所以要迭代去除($count返回替換的次數)。
完整的函數源碼:
function remove_invisible_characters($str, $url_encoded = TRUE) { $non_displayables = array(); if ($url_encoded) { $non_displayables[] = '/%0[0-8bcef]/'; // url encoded 00-08, 11, 12, 14, 15 $non_displayables[] = '/%1[0-9a-f]/'; // url encoded 16-31 } $non_displayables[] = '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S'; // 00-08, 11, 12, 14-31, 127 do { $str = preg_replace($non_displayables, '', $str, -1, $count); }while ($count); return $str; }
13. Html_escape
這個函數,實際上是數組中的元素遞歸調用htmlspecialchars。
函數實現源碼:
function html_escape($var) { if (is_array($var)) { return array_map('html_escape', $var); } else { return htmlspecialchars($var, ENT_QUOTES, config_item('charset')); } }
總結一下,Common.php是在各組件加載之前定義的一系列全局函數。這些全局函數的作用是獲取配置、跟蹤加載class、安全性過濾等。而這么做的目的之一,就是避免組件之間的過多依賴。
參考文獻:
PHP引用:http://www.cnblogs.com/xiaochaohuashengmi/archive/2011/09/10/2173092.html
HTTP協議:http://www.cnblogs.com/TankXiao/archive/2012/02/13/2342672.html