CI框架源代碼閱讀筆記3 全局函數Common.php

  從本篇開始。將深刻CI框架的內部。一步步去探索這個框架的實現、結構和設計。php

  Common.php文件定義了一系列的全局函數(通常來講。全局函數具備最高的載入優先權。所以大多數的框架中BootStrap引導文件都會最早引入全局函數,以便於以後的處理工做)。html

  打開Common.php中,第一行代碼就很詭異:nginx

if ( ! defined('BASEPATH')) exit('No direct script access allowed');

  上一篇(CI框架源代碼閱讀筆記2 一切的入口 index.php)中,咱們已經知道,BASEPATH是在入口文件裏定義的常量。這裏作這個推斷的緣由是:避免直接訪問文件,而必須由index.php入口文件進入。事實上不只是Common.php。System中所有文件。差點兒都要引入這個常量的推斷。避免直接的腳本訪問:windows

本文件裏定義的函數例如如下(查看方式 print_r(get_defined_functions())):api

 

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函數)

值得注意的是,現在很是多server內部擴展增長了本身定義的狀態碼。如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 */

因此你在查看server的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 client錯誤 */
        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_PARSEE_CORE_ERRORE_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

單例模式:http://cantellow.iteye.com/blog/838473

相關文章
相關標籤/搜索