CI框架源码解析八之地址解析类文件URI.php

        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);
    }

}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值