一個靈活可控的應用程序中,必然會存在大量的可控參數(我們稱為配置),例如在CI的主配置文件中(這裡指Application/Config/Config.php文件),有如下多項配置:
$config['base_url'] = 'http://test.xq.com'; $config['index_page'] = ''; $config['uri_protocol'] = 'AUTO'; $config['url_suffix'] = '.html'; $config['language'] = 'english'; $config['charset'] = 'UTF-8'; $config['enable_hooks'] = FALSE; …………………………
不僅如此,CI還允許你將配置參數放到主配置文件之外。例如,你可以定義自己的配置文件為Config_app.php, 然後在你的應用程序控制器中這樣加載你的配置文件:
$this->config->load('config_app');
如此紛繁多樣的配置項和配置文件,CI是如何進行管理的?這便是我們今天要跟蹤的內容:CI的配置管理組件-Config.php.
先看該組件的類圖:
其中:
_config_paths:要搜索的配置文件的路徑,這裡指APPPATH目錄,你的配置文件也應該位於APPPATH下。
Config: 這個數組用於存放所有的配置項的item
Is_loaded: 存放所有的已經加載的配置文件列表。
_construct: 組件的構造函數,主要是配置base_url
_assign_to_config: 允許index.php中的配置項覆蓋主配置文件中的設置
_uri_string,site_url,base_url,system_url: URI, 項目路徑等相關處理。
load: 加載配置文件。
item:獲取配置項
slash_item:同item,不同的是,在最後加了”\”分隔符,一般只有site_url,base_url等會需要slash_item
下面我們去剖析各個方法的具體實現:
之前我們在分析Common.php全局函數的時候提到過,在Config組件實例化之前,所有的組配置文件的獲取都是由get_config()函數來代理的。在Config組件實例化時,要將所有的配置存放到自己的私有變量$config中,便於之後的訪問和處理:
$this->config =& get_config();
由於我們應用程序很多時候需要獲取base_url的值,而這個值並不是必填項(config中base_url可以設置為空),但我們又不希望獲取到的base_url的值為空。因此,CI在Config組件初始化的時候,對base_url做了一定的處理。這主要出現在Config.php中base_url設置為空的情況:
(1). 如果設置了$_SERVER[‘HTTP_HOST’],則base_url被設置為Protocal(http或者https) + $_SERVER['HTTP_HOST'] + SCIRPT_PATH的形式:
$base_url = isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off' ? 'https' : 'http'; $base_url .= '://'. $_SERVER['HTTP_HOST']; $base_url .= str_replace(basename($_SERVER['SCRIPT_NAME']), '', $_SERVER['SCRIPT_NAME']);
(2). 否者,直接被設置為http://localhost/:
$base_url = 'http://localhost/';
(3). 同時將base_url配置項映射到配置數組中,方便之後的訪問(set_item方法我們稍後會將,這裡只需要知道,它是添加到配置項且會覆蓋舊值):
$this->set_item('base_url', $base_url);
之後我們會看到,base_url這個配置項對於很多組件都是必須的,因此,CI花費一定的精力來保證base_url的正確性,也是可以理解的。
這是Config組件中較核心的方法之一,該函數的簽名:
function load($file = '', $use_sections = FALSE, $fail_gracefully = FALSE)
所有的參數都是可選參數。
我們這裡簡單解釋一下各形參的含義:
$file 需要加載的配置文件,可以包含後綴名也不可以不包含,如果未指定該參數,則默認加載Config.php文件
$user_sections: 是否為加載的配置文件使用獨立的section,這麼說可能還是不明白,試想,如果你定義了自己的配置文件,而你的配置文件中的配置項可能與Config.php文件中的配置項沖突,通過指定$section為true可以防止配置項的覆蓋。
$fail_gracefully: 要load的配置文件不存在時的處理。Gracefully意為優雅的,如果該參數設置為true,則在文件不存在時只會返回false,而不會顯示錯誤。
下面看該方法的具體實現:
(1). 配置文件名預處理:
$file = ($file == '') ? 'config' : str_replace('.php', '', $file);
這個$file最後只包含文件名,而不包含擴展名。如果該參數為空,則默認加載Config.php配置文件。這同時也說明,我們加載自己的配置文件時:
$this->config->load("");與
$this->config->load("config")效果是一樣的,而:
$this->config->load("config_app")與
$this->config->load("config_app.php")的效果也是一樣的。
如果啟用了$use_sections,這個$file會作為config的主鍵。
(2). 查找和加載配置文件。
在跟蹤實現之前,先解釋幾個查找和加載過程中比較重要的參數:
(3).具體的查找過程是一個雙重的foreach循環:
/* 對於config_paths中的路徑循環查找 */ foreach ($this->_config_paths as $path) { /* 對每個location查找,也就是分別對ENVIRONMENT/config/ 和 config/ 目錄查找 */ foreach ($check_locations as $location) { /* 實際的配置文件名 */ $file_path = $path.'config/'.$location.'.php';
/* 如果已經加載,則跳至最外層循環,事實上,由於_config_paths的設定,會跳出整個循環 */ if (in_array($file_path, $this->is_loaded, TRUE)) { $loaded = TRUE; continue 2; } /* 若文件存在,跳出當前循環 */ if (file_exists($file_path)) { $found = TRUE; break; } } /* 如果沒有找到配置文件,繼續下一次循環。同樣,由於_config_path的設定,會跳出整個循環 */ if ($found === FALSE) { continue; } }
(4).引入配置文件
到這裡,如果配置文件不存在,則$found和$loaded都為false,CI會根據fail_gracefully參數決定文件不存在的處理方式;如果文件存在,則需要對配置文件的格式檢查:
/* 引入配置文件 */ include($file_path); /* 配置文件的格式檢查,這同時也說明,配置文件中最起碼應該包含$config數組 */ if ( ! isset($config) OR ! is_array($config)) { if ($fail_gracefully === TRUE) { return FALSE; } show_error('Your '.$file_path.' file does not appear to contain a valid configuration array.'); }
(5).對use_sections參數的處理
前面說過,use_secitons參數如果為true,則CI_Config會對該配置文件啟用獨立的key存儲。例如,我們在controller中這樣加載配置文件:
$this->config->load("config_app",true);
則config數組是這樣的格式:
[config] => Array ( [base_url] => http://test.xq.com [index_page] => [uri_protocol] => AUTO [url_suffix] => .html [proxy_ips] => [web_akey] => yyyyyyyyyyyy [config_app] => Array ( [web_akey] => xxxxxxx [web_skey] => xxxxxxxxxxxxxxxxxxx [web_callback_url] => http://test.xq.com/ [sess_pre] => WEB_APP [cart_min] => 1 [cart_max] => 999 ) )
相反,如果我們不指定use_sections,則數組是這樣存儲的:
[config] => Array ( [base_url] => http://test.xq.com [index_page] => [uri_protocol] => AUTO [url_suffix] => .html [web_akey] => xxxxxxx [web_skey] => xxxxxxxxxxxxxxxxxxx [web_callback_url] => http://test.xq.com/ [sess_pre] => WEB_APP [cart_min] => 1 [cart_max] => 999 )
這也意味著,在不啟用user_secitons的情況下,如果你的配置文件中有與主配置文件Config.php相同的鍵,則會覆蓋主配置文件中的項:
/* 啟用單獨的key存放加載的config */ if ($use_sections === TRUE) { if (isset($this->config[$file])) { $this->config[$file] = array_merge($this->config[$file], $config); } else { $this->config[$file] = $config; } } else { /* 執行merge,更改CI_Config::config */ $this->config = array_merge($this->config, $config); }
(6).錯誤處理
雙層循環完成後,如果loaded為false,也就是未成功加載任何配置,則根據fail_gracefully做相應的錯誤處理:
/* 未成功加載任何配置 */ if ($loaded === FALSE) { if ($fail_gracefully === TRUE) { return FALSE; } show_error('The configuration file '.$file.'.php does not exist.'); }
item方法用於在配置中獲取特定的配置項,改方法的簽名:
function item($item, $index = '')
注意,如果你在load配置文件的時候啟用了use-sections,則在使用item()獲取配置項的時候需要指定第二個參數,也就是加載的配置文件的文件名(不包含後綴)。為了更清楚這一點,我們假設現在Config/目錄下有配個配置文件:config.php和config_app.php,這兩個配置文件中含有一個相同的鍵web_akey, 在config.php中,該配置為:
$config['web_akey'] = 'yyyyyyyyyyyy';
而config_app.php中,該配置為:
$config['web_akey'] = 'xxxxxxx';
現在,通過use-sections的方法加載config_app配置文件(config.php會在Config組件初始化的時候被加載):
$this->config->load("config_app",true);
然後在控制器中獲取web_akey配置項:
echo "config_app:web_akey => ",$this->config->item("web_akey","config_app"),"<br/>"; echo "config :web_akey => ",$this->config->item("web_akey");
實際的獲取結果:
config_app:web_akey => xxxxxxx config :web_akey => yyyyyyyyyyyy
了解原理之後,該方法的實現就比較簡單了:
function item($item, $index = '') { /* 沒有設置use_sections的情況,直接在config中尋找配置項 */ if ($index == '') { if ( ! isset($this->config[$item])) { return FALSE; } $pref = $this->config[$item]; } else { if ( ! isset($this->config[$index])) { return FALSE; } if ( ! isset($this->config[$index][$item])) { return FALSE; } $pref = $this->config[$index][$item]; } /* 統一的return出口 */ return $pref; }
slash_item實際上與item()方法類似,但他不會去用戶的配置中尋找,並且,他返回的是主配置文件中的配置項,並在配置項最後添加反斜槓.這個方法,通常用於base_url和index_page這兩個配置項的處理:
該方法的實現源碼:
function slash_item($item) { /* 不存在配置項 */ if ( ! isset($this->config[$item])) { return FALSE; } /* 配置項為空 */ if( trim($this->config[$item]) == '') { return ''; } /* 去除最後的多余的"/",並在結尾添加一個"/" */ return rtrim($this->config[$item], '/').'/'; }
這裡先澄清這幾個含義的區別:
echo "site_url : ",$this->config->site_url("index/rain"),"</br>"; echo "base_url : ",$this->config->base_url("index/rain"),"<br/>"; echo "system_url: ",$this->config->system_url();
的結果分別是:
site_url : http://test.xq.com/index/rain.html base_url : http://test.xq.com/index/rain system_url: http://test.xq.com/system/
可以看出,site_url是添加了suffix(在Config/config.php中配置)後的url地址(呵呵,如果你的uri中有query string,則Ci總是在最後添加suffix:http://test.xq.com/index/rain?w=ss.html 是不是很奇怪.)
base_url則是沒有添加suffix的url地址。
而system_url這個東西很奇怪,是獲取系統的url路徑。但實際上,由於system路徑並沒有直接執行的腳本,所以這個方法的實際用途是什麼,暫時不知。有知道的童鞋麻煩告知。
具體的方法實現,這裡不贅述了。直接貼出源碼:
function site_url($uri = '') { /* 沒有設置uri,使用base_url + index_page */ if ($uri == '') { return $this->slash_item('base_url').$this->item('index_page'); } /* enable_query_strings未啟用,可以添加suffix後綴 */ if ($this->item('enable_query_strings') == FALSE) { $suffix = ($this->item('url_suffix') == FALSE) ? '' : $this->item('url_suffix'); return $this->slash_item('base_url').$this->slash_item('index_page').$this->_uri_string($uri).$suffix; } /* 否者不添加suffix後綴 */ else { return $this->slash_item('base_url').$this->item('index_page').'?'.$this->_uri_string($uri); } } /* 獲取base_url,注意與site_url的區別 */ function base_url($uri = '') { return $this->slash_item('base_url').ltrim($this->_uri_string($uri), '/'); } /* 獲取system url */ function system_url() { /* 獲取系統目錄. BASEPATH:/search/xx/phpCode/CI/system/ */ $x = explode("/", preg_replace("|/*(.+?)/*$|", "\\1", BASEPATH)); return $this->slash_item('base_url').end($x).'/'; }
site_url和base_url都調用了_uri_string。這個函數是做什麼用的呢?
按理來說, _uri_string的功能應該由URI組件來完成,這裡卻放在了Config組件中,似乎有些不妥(實際上,_uri_string是為base_url和site_url專屬服務的)。
對於這樣的uri:
array( 'p1' => 'param1', 'p2' => 'param2' )
如果enable_query_string為false,則_uri_string處理過後是這樣的形式:
param1/param2
而enable_query_string為true,則處理後的形式是這樣的:
p1=param1&p2=param2
這是我們常見(雖然很難看且SEO不好)的形式。改方法的實現源碼:
protected function _uri_string($uri) { /* enable_query_strings 為false,直接implode */ if ($this->item('enable_query_strings') == FALSE) { if (is_array($uri)) { $uri = implode('/', $uri); } $uri = trim($uri, '/'); } /* 否者,拼接成類似param1=param1¶m2=param2的形式 */ else { if (is_array($uri)) { $i = 0; $str = ''; foreach ($uri as $key => $val) { /* 第一個參數前面不需要加& */ $prefix = ($i == 0) ? '' : '&'; $str .= $prefix.$key.'='.$val; $i++; } $uri = $str; } } return $uri; }
與item()相反,set_item用於設置配置項。如果配置項已經存在,則會被覆蓋:
$this->config[$item] = $value;
_assign_to_config同set_item,該方法提供了數組的設置方式(調用set_item。我們之前在解釋CodeIgniter.php文件的時候提到過:改方法允許在index.php中設置獨立的配置項,且index.php中的配置具有更高的優先權(會覆蓋主配置文件中的配置):
function _assign_to_config($items = array()) { if (is_array($items)) { foreach ($items as $key => $val) { $this->set_item($key, $val); } } }
到這裡,Config組件的基本解析就算是完成了,我們再次回顧下該組件的基本功能:
最後感慨一下,一個好的Config組件,會省不少事啊。