URI类的作用主要是处理地址字符串,将URI分成对应的片段保存到segments,路由类也主要是通过segments数组来获取上下文中的URI请求信息
在阅读这段源码的时候,我们主要关心的问题有
1.URI类是如何将地址字符串解析成对应片段?
2.解析后的对应片段保存到变量中是怎样的数据结构?
1.__construct()构造函数
URI类在初始化的时候就会对地址进行解析,构造函数会根据不同的环境调用对应的解析函数,并保存解析结果。
cli模式:调用_parse_argv()进行解析
根据uri_protocol这个配置属性决定使用哪个解析函数
默认REQUEST_URI,使用_parse_request_uri解析函数
QUERY_STRING,使用_parse_query_string解析函数
PATH_INFO或其他参数,都使用_parse_request_uri解析函数
这些解析函数会将地址解析成uri字符串,再由_set_uri_string函数将uri字符串解析成对应片段
我们先看_construct的源码,之后再看具体的解析函数是如何实现的:
public function __construct()
{
//加载配置文件
$this->config =& load_class('Config', 'core');
//enable_query_strings参数为true的时候,直接从_GET数组中取出对应片段,所以不
//需要解析
if (is_cli() OR $this->config->item('enable_query_strings') !== TRUE)
{
//判断URI是否合法的正则表达式
$this->_permitted_uri_chars =
$this->config->item('permitted_uri_chars');
//如果是命令行模式
if (is_cli())
{
//_parse_argv()是实际进行解析的函数,解析完后结果返回给$uri
$uri = $this->_parse_argv();
}
else
{
//URI属性参数
$protocol = $this->config->item('uri_protocol');
//默认是REQUEST_URI
empty($protocol) && $protocol = 'REQUEST_URI';
//根据不同的URI属性调用对应的解析函数,并将结果返回给$uri
switch ($protocol)
{
case 'AUTO': // For BC purposes only
case 'REQUEST_URI':
$uri = $this->_parse_request_uri();
break;
case 'QUERY_STRING':
$uri = $this->_parse_query_string();
break;
case 'PATH_INFO':
default:
$uri = isset($_SERVER[$protocol])
? $_SERVER[$protocol]
: $this->_parse_request_uri();
break;
}
}
//将解析的uri字符串保存到uri_string中,对应片段存到segments变量
$this->_set_uri_string($uri);
}
log_message('info', 'URI Class Initialized');
}
2.命令行模式下_parse_argv()解析函数
这个uri字符串解析函数,是从$_SERVER[‘argv’]中获取参数
protected function _parse_argv()
{
//取出所有参数
$args = array_slice($_SERVER['argv'], 1);
//以'/'连接参数,组成uri字符串
return $args ? implode('/', $args) : '';
}
3._parse_request_uri()解析函数
从$_SERVER[‘REQUEST_URI’]中获取参数,
protected function _parse_request_uri()
{
//如果$_SERVER['REQUEST_URI']或$_SERVER['SCRIPT_NAME']为空直接返回空
if ( ! isset($_SERVER['REQUEST_URI'], $_SERVER['SCRIPT_NAME']))
{
return '';
}
//获取URL的query,path
$uri = parse_url('http://dummy'.$_SERVER['REQUEST_URI']);
$query = isset($uri['query']) ? $uri['query'] : '';
$uri = isset($uri['path']) ? $uri['path'] : '';
//去掉uri字符串中脚本名即xxx.php开头的字符串部分和脚本名所在目录名开头的字符串部
//分
if (isset($_SERVER['SCRIPT_NAME'][0]))
{
if (strpos($uri, $_SERVER['SCRIPT_NAME']) === 0)
{
$uri = (string) substr($uri,strlen($_SERVER['SCRIPT_NAME']));
}
elseif (strpos($uri, dirname($_SERVER['SCRIPT_NAME'])) === 0)
{
$uri = (string) substr($uri,
strlen(dirname($_SERVER['SCRIPT_NAME'])));
}
}
//当出现http://localhost.com/path?/test?key=val时,只将key=val作为get参
//数
if (trim($uri, '/') === '' && strncmp($query, '/', 1) === 0)
{
$query = explode('?', $query, 2);
$uri = $query[0];
$_SERVER['QUERY_STRING'] = isset($query[1]) ? $query[1] : '';
}
else
{
$_SERVER['QUERY_STRING'] = $query;
}
//将查询字符串解析到$_GET数组中
parse_str($_SERVER['QUERY_STRING'], $_GET);
if ($uri === '/' OR $uri === '')
{
return '/';
}
//删除多余的斜杠,返回uri字符串
return $this->_remove_relative_directory($uri);
}
4._parse_query_string()解析函数
根据$_SERVER[‘QUERY_STRING’]的参数解析出Uri字符串
protected function _parse_query_string()
{
//获取$_SERVER['QUERY_STRING']参数
$uri = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING']
: @getenv('QUERY_STRING');
//之后的代码与_parse_request_uri()函数一样
if (trim($uri, '/') === '')
{
return '';
}
elseif (strncmp($uri, '/', 1) === 0)
{
$uri = explode('?', $uri, 2);
$_SERVER['QUERY_STRING'] = isset($uri[1]) ? $uri[1] : '';
$uri = $uri[0];
}
parse_str($_SERVER['QUERY_STRING'], $_GET);
return $this->_remove_relative_directory($uri);
}
5._remove_relative_directory()——去掉多余斜杠和相对路径符号
protected function _remove_relative_directory($uri)
{
//以'/'分割uri字符串并保存到$uris数组中,将'..'字符串去掉
$uris = array();
$tok = strtok($uri, '/');
while ($tok !== FALSE)
{
if (( ! empty($tok) OR $tok === '0') && $tok !== '..')
{
$uris[] = $tok;
}
$tok = strtok('/');
}
//用'/'重新拼接数组
return implode('/', $uris);
}
6.uri解析成对应分段——_set_uri_string()函数
前面解读的几个函数已经将url解析成uri字符串了,但是我们最终需要的是Uri字符串所对应的参数,这样才能根据uri参数路由到正确的位置,set_uri_string()函数的功能便是将uri字符串解析成对应分段
protected function _set_uri_string($str)
{
//移除$str中的隐藏字符串和字符串两侧的'/',并将$str的值赋给$this->uri_string
$this->uri_string =
trim(remove_invisible_characters($str, FALSE), '/');
if ($this->uri_string !== '')
{
//如果配置文件中配置了url后缀,则移除后缀
if (($suffix = (string) $this->config->item('url_suffix'))
!== '')
{
$slen = strlen($suffix);
if (substr($this->uri_string, -$slen) === $suffix)
{
$this->uri_string = substr($this->uri_string, 0, -$slen);
}
}
$this->segments[0] = NULL;
//将uri_string字符串以'/'分割成数组,并将数组的元素存储到segments数组中
//segments数组下标由1开始
foreach (explode('/', trim($this->uri_string, '/')) as $val)
{
$val = trim($val);
//判断是否符合参数是否合法
$this->filter_uri($val);
if ($val !== '')
{
$this->segments[] = $val;
}
}
unset($this->segments[0]);
}
}
7.合法性保障——filter_uri函数
public function filter_uri(&$str)
{
//参数为空或者不符合uri正则表达式,则参数不合法,展示错误界面
if ( ! empty($str) && ! empty($this->_permitted_uri_chars) &&
! preg_match('/^['.$this->_permitted_uri_chars.']+$/i'.
(UTF8_ENABLED ? 'u' : ''), $str))
{
show_error('The URI you submitted has disallowed characters.
', 400);
}
}
总结:
以上几个函数则是URI.php中的主要函数了,还有将uri建立关联数组,获取segments这些函数,我只阅读了一下主要函数的代码,那些有兴趣的时候再看吧。
这里回答一下开头的两个问题:
1.URI类是如何将地址字符串解析成对应片段?
答:URI类首先将URL字符串解析成URI字符串,URI字符串的格式则是我们已经非常熟悉的CI路由地址(查询字符串,SCRIPT_NAME,以及SCRIPT_NAME目录名不会出现在uri字符串中),然后再将URI字符串中的参数解析出来存储到segments数组中,这样就讲url解析成对应的片段了。
2.解析后的对应片段保存到变量中是怎样的数据结构?
答:解析出来的片段从下标1开始依次保存到segments数组中。