URI类主要处理地址字符串,将uri分解成对应的片段,存到segments数组中。querystring分解后存到$_GET数组,ROUTER路由类在之后的解析路由动作中,也主要依靠URI类的segments属性数组来获取当前上下文的请求URI信息。
在CI框架中如果允许传统的querystirng模式,即设置$config['enable_query_strings'] = TRUE,URI类将不做任何处理,ROUTER类也只会匹配目录、控制器、方法。CI框架体系中的方法参数都是从URI片段中取的,并按顺序传递给方法参数。不支持将querstring中的变量通过方法参数名传给方法,只能用$_GET获取。
$config['uri_protocol']配置不但决定以哪个函数处理URI,同时决定了从哪个全局变量里获取当前上下文的uri地址。对应关系是:
'REQUEST_URI' 使用 $_SERVER['REQUEST_URI']
'QUERY_STRING' 使用 $_SERVER['QUERY_STRING']
'PATH_INFO' 使用 $_SERVER['PATH_INFO']
那么这三个变量有什么区别呢?
$_SERVER['REQUEST_URI']获取的是url地址中主机头后面所有的字符
$_SERVER['QUERY_STRING']获取的url地址中"?"后面的部分
$_SERVER['PATH_INFO']获取的是url地址中脚本文件($_SERVER['SCRIPT_NAME'])之后"?"之前的字符内容
下面分析URI类中的几个重要函数:
1、构造函数_construct()
功能及流程:解析命令行或url地址,获取querystring存入_GET全局数组中,并返回删除了入口文件的url地址;再调用$this->_set_uri_string($uri)生成segments数组。URI类对于存入GET中的参数,并不做安全处理(安全处理在INPUT类中实现)。
① 处理命令行参数$uri = $this->_parse_argv();
使用场景:我们用命令执行PHP脚本:[root@twm ~]#php index.php mall lists。即:$uri="mall/lists"
protected function _parse_argv() { $args = array_slice($_SERVER['argv'], 1); return $args ? implode('/', $args) : ''; }
② 获取querystring存入_GET全局数组中,并返回删除入口文件的url地址
switch ($protocol) { case 'AUTO': // For BC purposes only case 'REQUEST_URI': //这种REQUEST_URI方式相对复杂一点,因此封装在$this->_parse_request_uri();里面。 //其实大多数情况下,利用REQUEST URI和SCRIPT NAME都会得到我们想要的路径信息了。 $uri = $this->_parse_request_uri(); break; case 'QUERY_STRING': //如果是用QUERY_STRING的话,路径格式一般为index.php?/controller/method/xxx/xxx $uri = $this->_parse_query_string(); break; case 'PATH_INFO': //PATH_INFO方式,个人觉得这种方式最经济,只是不是每次请求都有$_SERVER['PATH_INFO']这个变量。 default: //上面的方法都不行,那真是奇怪了。。所以尝试最后一种奇葩的方法,就是从$_GET里面把那个键名拿出来。 $uri = isset($_SERVER[$protocol]) ? $_SERVER[$protocol] : $this->_parse_request_uri(); break; }
可以看出:
_parse_request_uri方法处理 AUTO,REQUEST_URI,PATH_INFO
_parse_query_string方法只处理QUERY_STRING
③ 调用$this->_set_uri_string($uri)生成segments数组。
2、_parse_request_uri()方法注释
protected function _parse_request_uri() { if (!isset($_SERVER['REQUEST_URI'], $_SERVER['SCRIPT_NAME'])) { return ''; } //从$_SERVER['REQUEST_URI']取值,解析成$uri和$query两个字符串,分别存储请求的路径和get请求参数 $uri = parse_url('http://dummy' . $_SERVER['REQUEST_URI']); $query = isset($uri['query']) ? $uri['query'] : ''; $uri = isset($uri['path']) ? $uri['path'] : ''; //去掉uri包含的$_SERVER['SCRIPT_NAME'], //比如uri是http://www.citest.com/index.php/news/view/crm,经过处理后就变成/news/view/crm了 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']))); } } //对于请求服务器的具体URI包含在查询字符串这种情况。 //例如$uri以?/开头的 ,实际上if条件换种写法就是if(strncmp($uri, '?/', 2) === 0)),类似: //http://www.citest.com/index.php?/welcome/index 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; 如下面这种请求uri: //http://www.citest.com/mall/lists?page=7 $_SERVER['QUERY_STRING'] = $query; } //将查询字符串按键名存入_GET数组 parse_str($_SERVER['QUERY_STRING'], $_GET); if ($uri === '/' OR $uri === '') { return '/'; } //调用 _remove_relative_directory($uri)函数作安全处理 //移除$uri中的../相对路径字符和反斜杠/ return $this->_remove_relative_directory($uri); }
3、_parse_query_string()方法注释
protected function _parse_query_string() { //从$_SERVER['QUERY_STRING']取值 $uri = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : @getenv('QUERY_STRING'); //对于没有实际内容的,直接返回空。 if (trim($uri, '/') === '') { return ''; } elseif (strncmp($uri, '/', 1) === 0) { //对应生成$_SERVER['QUERY_STRING']和$uri //最后将$_SERVER['QUERY_STRING']解析于_GET数组parse_str($_SERVER['QUERY_STRING'], $_GET); $uri = explode('?', $uri, 2); $_SERVER['QUERY_STRING'] = isset($uri[1]) ? $uri[1] : ''; $uri = $uri[0]; } //将查询字符串按键名存入_GET数组 parse_str($_SERVER['QUERY_STRING'], $_GET); //调用 _remove_relative_directory($uri)函数作安全处理 //移除$uri中的../相对路径字符和反斜杠/ return $this->_remove_relative_directory($uri); }
4、_set_uri_string($str)方法,解析$url填充到$this->segments数组中去
/** * 就是给uri_string赋值 * 解析$url填充到$this->segments数组中去 */ protected function _set_uri_string($str) { //移除$str不可见字符: $this->uri_string=trim(remove_invisible_characters($str, FALSE), '/') //这样做的意义在于防止在字符中间夹入空字符造成漏洞,比如Java\0script $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; //解析$url,用"/"分段,填充到$this->segments数组中去 foreach (explode('/', trim($this->uri_string, '/')) as $val) { $val = trim($val); $this->filter_uri($val); if ($val !== '') { $this->segments[] = $val; } } unset($this->segments[0]); } }
5、to_assoc函数簇:uri_to_assoc($n = 3, $default = array()) , ruri_to_assoc($n = 3, $default = array())
该方法用于将 URI 的段转换为一个包含键值对的关联数组。如下 URI:http://www.citest.com/user/search/name/joe/location/UK/gender/male
使用这个方法可以将 URI 转为如下的数组原型:
[array]
(
'name' => 'joe'
'location' => 'UK'
'gender' => 'male'
)
可以通过第一个参数设置一个位移,默认值为 3 ,这是因为URI 的前两段通常都是控制器和方法。 例如:$array = $this->uri->uri_to_assoc(3);echo $array['name'];第二个参数用于设置默认的键名,这样即使 URI 中缺少某个键名,也能保证返回的数组中包含该索引。 例如:$default = array('name', 'gender', 'location', 'type', 'sort');$array = $this->uri->uri_to_assoc(3, $default);按照CI体系,segments前两个对应的分别是控制器名和方法名,所以默认从第三个取。
6、segment()函数簇
① segment($n, $no_result = NULL) , rsegment($n, $no_result = NULL)
用于从 URI 中获取指定段。参数 n 为你希望获取的段序号,URI 的段从左到右进行编号。 例如,如果你的完整 URL 是这样的:http://www.citest.com/index.php/news/local/metro/crime_is_up;那么每个分段如下:
news
local
metro
crime_is_up
$product_id = $this->uri->segment(3, 0);返回metro,不存在时返回0
② segment_array() , rsegment_array()
返回 URI 所有的段组成的数组。例如:
$segs = $this->uri->segment_array(); foreach ($segs as $segment) { echo $segment; echo '<br />'; }
③ total_segments() , total_rsegments()
返回URI的总段数。
④ slash_segment($n, $where = 'trailing') , slash_rsegment($n, $where = 'trailing')
该方法和 segment() 类似,只是它会根据第二个参数在返回结果的前面或/和后面添加斜线。 如果第二个参数未设置,斜线会添加到后面,根据源代码看,如果第二个参数不是trailing,也不是leading,将会在头尾都加斜杠。
protected function _slash_segment($n, $where = 'trailing', $which = 'segment') { $leading = $trailing = '/'; if ($where === 'trailing') { $leading = ''; } elseif ($where === 'leading') { $trailing = ''; } return $leading . $this->$which($n) . $trailing; }
7、uri_string() , ruri_string()
这两个函数返回一个相对的 URI 字符串,例如,如果你的完整 URL 为:http://www.citest.com/index.php/news/local/345
该方法返回:
news/local/345
最后,贴一下整个地址解析类URI.php文件的源码(注释版):
<?php /** * ======================================= * Created by Pocket Knife Technology. * User: ZhiHua_W * Date: 2016/10/20 0110 * Time: 下午 2:08 * Project: CodeIgniter框架—源码分析 * Power: Analysis for URI.php * ======================================= */ //不说 defined('BASEPATH') OR exit('No direct script access allowed'); /** * 解析uri,确定路由 * URI类主要处理地址字符串,将uri分解成对应的片段,存到segments数组中。 * querystring分解后存到$_GET数组 * ROUTER路由类在之后的解析路由动作中,也主要依靠URI类的segments属性数组来获取当前上下文的请求URI信息。 */ class CI_URI { //缓存uri片段 public $keyval = array(); //当前的uri片段 public $uri_string = ''; //URI片段数组 数组键值从0开始 public $segments = array(); //重建索引的片段数组 数组键值从1开始 public $rsegments = array(); //URI段允许PCRE字符组 protected $_permitted_uri_chars; //构造函数,需要获取config文件中的配置 public function __construct() { $this->config =& load_class('Config', 'core'); //如果启用了查询字符串,我们不需要解析任何部分。 //如果你在配置文件config.php里面把这个enable_query_strings定义成一种上面都没有的方式,那么就会执行下面的代码。 if (is_cli() OR $this->config->item('enable_query_strings') !== TRUE) { $this->_permitted_uri_chars = $this->config->item('permitted_uri_chars'); //如果它是一个CLI配置要求,忽视 if (is_cli()) { $uri = $this->_parse_argv(); } else { //下面的uri_protocol是在config.php里面的一个配置项,其实是问你用哪种方式去检测uri的信息的意思, //默认是AUTO,自动检测,也就是通过各种方式检测,直至检测到,或者全部方式都检测完。 $protocol = $this->config->item('uri_protocol'); empty($protocol) && $protocol = 'REQUEST_URI'; //开始尝试各种方式,主要有:命令行,REQUEST_URI, PATH_INFO, QUERY_STRING. //下面会多次出现$this->_set_uri_string($str)这个方法,这个方法没别的,就是把$str经过 //过滤和修剪后值给$this->uri_string属性,在这里暂时可以理解为就是赋值。 //如果脚本是在命令行模式下运行的话,那么参数就是通过$_SERVER['argv']来传递。下面的 //$this->_parse_cli_args();就是拿到符合我们需要的路由相关的一些参数了 switch ($protocol) { case 'AUTO': // For BC purposes only case 'REQUEST_URI': //这种REQUEST_URI方式相对复杂一点,因此封装在$this->_parse_request_uri();里面。 //其实大多数情况下,利用REQUEST URI和SCRIPT NAME都会得到我们想要的路径信息了。 $uri = $this->_parse_request_uri(); break; case 'QUERY_STRING': //如果是用QUERY_STRING的话,路径格式一般为index.php?/controller/method/xxx/xxx $uri = $this->_parse_query_string(); break; case 'PATH_INFO': //PATH_INFO方式,个人觉得这种方式最经济,只是不是每次请求都有$_SERVER['PATH_INFO']这个变量。 default: //上面的方法都不行,那真是奇怪了。。所以尝试最后一种奇葩的方法,就是从$_GET里面把那个键名拿出来。 $uri = isset($_SERVER[$protocol]) ? $_SERVER[$protocol] : $this->_parse_request_uri(); break; } //可以看出: //_parse_request_uri方法处理 AUTO,REQUEST_URI,PATH_INFO //_parse_query_string方法只处理QUERY_STRING } $this->_set_uri_string($uri); } log_message('info', 'URI Class Initialized'); } /** * 就是给uri_string赋值 * 解析$url填充到$this->segments数组中去 */ protected function _set_uri_string($str) { //移除$str不可见字符: $this->uri_string=trim(remove_invisible_characters($str, FALSE), '/') //这样做的意义在于防止在字符中间夹入空字符造成漏洞,比如Java\0script $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; //解析$url,用"/"分段,填充到$this->segments数组中去 foreach (explode('/', trim($this->uri_string, '/')) as $val) { $val = trim($val); $this->filter_uri($val); if ($val !== '') { $this->segments[] = $val; } } unset($this->segments[0]); } } protected function _parse_request_uri() { if (!isset($_SERVER['REQUEST_URI'], $_SERVER['SCRIPT_NAME'])) { return ''; } //从$_SERVER['REQUEST_URI']取值,解析成$uri和$query两个字符串,分别存储请求的路径和get请求参数 $uri = parse_url('http://dummy' . $_SERVER['REQUEST_URI']); $query = isset($uri['query']) ? $uri['query'] : ''; $uri = isset($uri['path']) ? $uri['path'] : ''; //去掉uri包含的$_SERVER['SCRIPT_NAME'], //比如uri是http://www.citest.com/index.php/news/view/crm,经过处理后就变成/news/view/crm了 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']))); } } //对于请求服务器的具体URI包含在查询字符串这种情况。 //例如$uri以?/开头的 ,实际上if条件换种写法就是if(strncmp($uri, '?/', 2) === 0)),类似: //http://www.citest.com/index.php?/welcome/index 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; 如下面这种请求uri: //http://www.citest.com/mall/lists?page=7 $_SERVER['QUERY_STRING'] = $query; } //将查询字符串按键名存入_GET数组 parse_str($_SERVER['QUERY_STRING'], $_GET); if ($uri === '/' OR $uri === '') { return '/'; } //调用 _remove_relative_directory($uri)函数作安全处理 //移除$uri中的../相对路径字符和反斜杠/ return $this->_remove_relative_directory($uri); } protected function _parse_query_string() { //从$_SERVER['QUERY_STRING']取值 $uri = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : @getenv('QUERY_STRING'); //对于没有实际内容的,直接返回空。 if (trim($uri, '/') === '') { return ''; } elseif (strncmp($uri, '/', 1) === 0) { //对应生成$_SERVER['QUERY_STRING']和$uri //最后将$_SERVER['QUERY_STRING']解析于_GET数组parse_str($_SERVER['QUERY_STRING'], $_GET); $uri = explode('?', $uri, 2); $_SERVER['QUERY_STRING'] = isset($uri[1]) ? $uri[1] : ''; $uri = $uri[0]; } //将查询字符串按键名存入_GET数组 parse_str($_SERVER['QUERY_STRING'], $_GET); //调用 _remove_relative_directory($uri)函数作安全处理 //移除$uri中的../相对路径字符和反斜杠/ return $this->_remove_relative_directory($uri); } //把每一个命令行参数,假设它是一个URI段。 protected function _parse_argv() { $args = array_slice($_SERVER['argv'], 1); return $args ? implode('/', $args) : ''; } // _remove_relative_directory($uri)函数作安全处理,移除$uri中的../相对路径字符和反斜杠 protected function _remove_relative_directory($uri) { $uris = array(); $tok = strtok($uri, '/'); while ($tok !== FALSE) { if ((!empty($tok) OR $tok === '0') && $tok !== '..') { $uris[] = $tok; } $tok = strtok('/'); } return implode('/', $uris); } //过滤不合法的url字符,允许的uri是你的配置$config['permitted_uri_chars'] = 'a-z 0-9~%.:_\-'; public function filter_uri(&$str) { 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 中获取指定段。参数 n 为你希望获取的段序号, * URI 的段从左到右进行编号。 */ public function segment($n, $no_result = NULL) { return isset($this->segments[$n]) ? $this->segments[$n] : $no_result; } //返回确定路由后的一个uri片段 public function rsegment($n, $no_result = NULL) { return isset($this->rsegments[$n]) ? $this->rsegments[$n] : $no_result; } /** * 该方法用于将 URI 的段转换为一个包含键值对的关联数组 */ public function uri_to_assoc($n = 3, $default = array()) { return $this->_uri_to_assoc($n, $default, 'segment'); } //相同的ci_uri::uri_to_assoc(),只有通过重新路由段阵列。 public function ruri_to_assoc($n = 3, $default = array()) { return $this->_uri_to_assoc($n, $default, 'rsegment'); } //生成的URI字符串或重新路由URI字符串键-值对 protected function _uri_to_assoc($n = 3, $default = array(), $which = 'segment') { if (!is_numeric($n)) { return $default; } if (isset($this->keyval[$which], $this->keyval[$which][$n])) { return $this->keyval[$which][$n]; } $total_segments = "total_{$which}s"; $segment_array = "{$which}_array"; if ($this->$total_segments() < $n) { return (count($default) === 0) ? array() : array_fill_keys($default, NULL); } $segments = array_slice($this->$segment_array(), ($n - 1)); $i = 0; $lastval = ''; $retval = array(); foreach ($segments as $seg) { if ($i % 2) { $retval[$lastval] = $seg; } else { $retval[$seg] = NULL; $lastval = $seg; } $i++; } if (count($default) > 0) { foreach ($default as $val) { if (!array_key_exists($val, $retval)) { $retval[$val] = NULL; } } } isset($this->keyval[$which]) OR $this->keyval[$which] = array(); $this->keyval[$which][$n] = $retval; return $retval; } //很明显,它是将数组中的信息翻转成uri_string public function assoc_to_uri($array) { $temp = array(); foreach ((array)$array as $key => $val) { $temp[] = $key; $temp[] = $val; } return implode('/', $temp); } //通过第二个参数看是否给uri前后加上“/”线 public function slash_segment($n, $where = 'trailing') { return $this->_slash_segment($n, $where, 'segment'); } //取一个URI路由段斜线 public function slash_rsegment($n, $where = 'trailing') { return $this->_slash_segment($n, $where, 'rsegment'); } /** * 该方法和 segment() 类似,只是它会根据第二个参数在返回结果的前面或/和后面添加斜线。 * 如果第二个参数未设置,斜线会添加到后面根据源代码看, * 如果第二个参数不是trailing,也不是leading,将会在头尾都加斜杠。 */ protected function _slash_segment($n, $where = 'trailing', $which = 'segment') { $leading = $trailing = '/'; if ($where === 'trailing') { $leading = ''; } elseif ($where === 'leading') { $trailing = ''; } return $leading . $this->$which($n) . $trailing; } /** * 返回 URI 所有的段组成的数组。 */ public function segment_array() { return $this->segments; } /** * 返回 URI 所有的段组成的数组。 */ public function rsegment_array() { return $this->rsegments; } /** * 返回 URI 的总段数 */ public function total_segments() { return count($this->segments); } /** * 返回 URI 的总段数 */ public function total_rsegments() { return count($this->rsegments); } //返回一个相对的 URI 字符串 public function uri_string() { return $this->uri_string; } /** * 返回一个相对的 URI 字符串, */ public function ruri_string() { return ltrim(load_class('Router', 'core')->directory, '/') . implode('/', $this->rsegments); } }