一个灵活可控的应用程序中,必然会存在大量的可控参数(我们称为配置),例如在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
下面我们去剖析各个方法的具体实现:
1. 组件初始化 _construct
之前我们在分析Common.php全局函数的时候提到过,在Config组件实例化之前,所有的组配置文件的获取都是由get_config()函数来代理的。在Config组件实例化时,要将所有的配置存放到自己的私有变量$config中,便于之后的访问和处理:
1
|
$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的形式:
1
2
3
|
$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/:
1
|
|
(3). 同时将base_url配置项映射到配置数组中,方便之后的访问(set_item方法我们稍后会将,这里只需要知道,它是添加到配置项且会覆盖旧值):
1
|
$this
->set_item(
'base_url'
,
$base_url
);
|
之后我们会看到,base_url这个配置项对于很多组件都是必须的,因此,CI花费一定的精力来保证base_url的正确性,也是可以理解的。
2. 加载配置文件 load
这是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). 查找和加载配置文件。
在跟踪实现之前,先解释几个查找和加载过程中比较重要的参数:
- $found 这个参数实际上是个flag,用于标识配置文件是否查找到,一旦查找到配置文件,则停止任何搜索。
- $loaded 同$found参数类似,这个$loaded也是一个flag,用于标识请求的配置文件是否被加载。一般情况下,被加载的配置文件会被CI_Config:: is_loaded变量追踪
- $_config_path 要查找的配置路径,这个变量由于是写死在Config组件中的,且没有提供添加或者更改的接口。因此我们可以认为_config_path就是APPPATH.也就是,配置文件的load一定是在APPPATH目录下查找的。
- $check_locations 这个参数是要查找的位置(具体文件)。同样,如果定了ENVIRONMENT且存在相应ENVIRONMENT下的配置文件,优先加载该文件。
(3).具体的查找过程是一个双重的foreach循环:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
/* 对于config_paths中的路径循环查找 */
foreach
(
$this
->_config_paths
as
$path
)
{
/* 对每个location查找,也就是分别对ENVIRONMENT/config/ 和 config/ 目录查找 */
foreach
(
$check_locations
as
$location
)
{
/* 实际的配置文件名 */
$file_path
=
$path
.
'config/'
.
$location
.
'.php'
;
<br>
/* 如果已经加载,则跳至最外层循环,事实上,由于_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参数决定文件不存在的处理方式;如果文件存在,则需要对配置文件的格式检查:
1
2
3
4
5
6
7
8
9
10
11
12
|
/* 引入配置文件 */
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相同的键,则会覆盖主配置文件中的项:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/* 启用单独的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做相应的错误处理:
1
2
3
4
5
6
7
8
9
|
/* 未成功加载任何配置 */
if
(
$loaded
=== FALSE)
{
if
(
$fail_gracefully
=== TRUE)
{
return
FALSE;
}
show_error(
'The configuration file '
.
$file
.
'.php does not exist.'
);
}
|
3. 获取配置项item,slash_item
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组件初始化的时候被加载):
1
|
$this
->config->load(
"config_app"
,true);
|
然后在控制器中获取web_akey配置项:
1
2
|
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
了解原理之后,该方法的实现就比较简单了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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这两个配置项的处理:
该方法的实现源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
function
slash_item(
$item
)
{
/* 不存在配置项 */
if
( ! isset(
$this
->config[
$item
]))
{
return
FALSE;
}
/* 配置项为空 */
if
( trim(
$this
->config[
$item
]) ==
''
)
{
return
''
;
}
/* 去除最后的多余的"/",并在结尾添加一个"/" */
return
rtrim(
$this
->config[
$item
],
'/'
).
'/'
;
}
|
4. 获取站点site_url, base_url,system_url
这里先澄清这几个含义的区别:
1
2
3
|
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路径并没有直接执行的脚本,所以这个方法的实际用途是什么,暂时不知。有知道的童鞋麻烦告知。
具体的方法实现,这里不赘述了。直接贴出源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
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
).
'/'
;
}
|
5. 获取URI String: _uri_string
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不好)的形式。改方法的实现源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
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
;
}
|
6. 设置配置项 set_item _assign_to_config
与item()相反,set_item用于设置配置项。如果配置项已经存在,则会被覆盖:
1
|
$this
->config[
$item
] =
$value
;
|
_assign_to_config同set_item,该方法提供了数组的设置方式(调用set_item。我们之前在解释CodeIgniter.php文件的时候提到过:改方法允许在index.php中设置独立的配置项,且index.php中的配置具有更高的优先权(会覆盖主配置文件中的配置):
1
2
3
4
5
6
7
8
9
10
|
function
_assign_to_config(
$items
=
array
())
{
if
(
is_array
(
$items
))
{
foreach
(
$items
as
$key
=>
$val
)
{
$this
->set_item(
$key
,
$val
);
}
}
}
|
到这里,Config组件的基本解析就算是完成了,我们再次回顾下该组件的基本功能:
- set_item和item是Config组件的基本对外接口。也就是常见的setter 和getter,_assign_to_config算是批量的setter,slash_item则是特殊处理的getter
- load方法是加载配置文件,如果你自定义了自己的配置文件,需要先load使得你的配置纳入CI_Config的管理之下。
- system_url,base_url,site_url,用于获取特定的配置项。
- _uri_string是CI_Config中唯一一个Protected的方法。这个方法主要是处理uri,提供给site_url和base_url使用
最后感慨一下,一个好的Config组件,会省不少事啊。