CodeIgniter源码分析,看不懂的路由规则

作者: 仪器仪表  发布:2019-10-07

compatible

上一节我们说url的请求本质上是调用控制器的方法,我们分析了下确实也是如此,最终是在CodeIgniter.php中从load_class函数中生成的路由对像$RTR上获取了控制器,方法和参数,并使用call_user_func_array实现了对控制器的方法的调用。但是$RTR对象上的控制器和方法到底是怎么解析来的?

路由这部分貌似在实际工作中没有怎么设计过,只是在用默认的设置,在手册里面看到部分,艰涩难懂。

对于配置pathinfo的支持,在Nginx作服务器、无数种系统要同时运行的环境,实在是一项很累赘的事情,而又不想很low的多个参数(像m、c、a)构造路由参数,我需要那种不必强制使用pathinfo的还可以伪pathinfo(用一个路由参数如s=/abc/ddd/ddd.html,参数名如route、s、r等)的框架。

这一节我们进入Router.php中看看CI框架的url解析和路由处理。在解析路由之前,先说下CI支持的url风格以及路由配置。

 

目前TP的v5版本下,仅可支持路由解析时的Compat模式,而不支持路由构建时的Compat模式。

CI框架的url

CI框架支持三种类型的url分别为

  • url分段(pathinfo)模式,例如 example.com/Test/index/param_value,这种方式是CI默认支持的url模式,这种url非常直观,域名后面的是url分段信息
    • Test对应的是控制器类
    • index对应的是我们要调用的类方法
    • param_value对应的是我们传递给方法的参数
  • 伪静态的模式,例如 example.com/Test/index.shtml,这种url模式让页面看起来像静态页面一样,这种url有利于SEO优化
  • 查询字符串模式,例如

    example.com?c=test&m=index¶m=param_value,这种url模式需要在config.php文件中开启($config['enable_query_strings']

    true),并可配置相应的查询字段用来获取控制器,方法等,查询字符串字段的含义如下
    • c为控制器类
    • m为类方法
    • param为参数

1.路由定义

增加一个配置项URL_MODE,找到构建Url的类thinkUrl::build方法,在参数组装的部分和脚本名与参数连接处做文章。

CI框架的路由配置

由于有时候出于某些目的我们并不想暴露我们的控制器和方法,或者我们想对访问的资源做一些区分,这时候我们就可以借助路由机制来实现我们的这种需求,路由配置文件位于applications/config/routes.php中,我们可以配置相关的路由和默认控制器
CI框架的路由支持路由配置方式分别为(这些url风格都属于pathinfo)

  • 通配符方式,例如 $route['index/(:any)'] = 'Test/index/$1';
    • (:any)是url中的参数,它是后面$1的值。
  • 正则表达式,例如 $route['([a-z]+)/(d+)'] = 'Test/$1/$2/';如果我们访问一个形如 example.com/index/2这样的url,利用正则表达式的规则,依然会解析出pathinfo模式的url(Test/index/2)
    • $1,$2是对正则表达式子匹配的逆向引用
  • 回调函数,例如 $route[''([a-z]+)/(d+)''] = callback($method, $id),不同于通配符和正则表达式的硬编码映射url,我们可以在回调函数中动态返回要映射的url
  • REST风格的路由,例如 $route['index']['put'] = 'Test/index';这种路由模式一般用在Rest API中

好了,通过上面的描述我们发现url的风格和路由的配置多种多样,想想路由解析都觉得好复杂,我们进入Router.php中看看CI框架的url解析和路由处理是怎么样的

要使用路由功能需要支持PATH_INFO,PATH_INFO是什么呢?手册中提到“要使用路由功能,前提是你的URL支持PATH_INFO(或者兼容URL模式也可以,采用普通URL模式的情况下不支持路由功能),” , url支持path_info,不是apache要支持path_info么,度娘讲的还算清楚一点,见下文:

// 参数组装
        if (!empty($vars)) {
            // 添加参数
            if (Config::get('url_common_param')) {
                $vars = urldecode(http_build_query($vars));
                $url .= $suffix . ((Config::get('URL_MODE') == static::MODE_COMPAT) ? '&' : '?') . $vars . $anchor;
            } else {
                $paramType = Config::get('url_param_type');
                foreach ($vars as $var => $val) {
                    if ('' !== trim($val)) {
                        if ($paramType) {
                            $url .= $depr . urlencode($val);
                        } else {
                            $url .= $depr . $var . $depr . urlencode($val);
                        }
                    }
                }
                $url .= $suffix . $anchor;
            }
        } else {
            $url .= $suffix . $anchor;
        }
        // 检测域名
        $domain = self::parseDomain($url, $domain);
        // URL组装
        $path_sep = '/';
        if (Config::get('URL_MODE') == static::MODE_COMPAT) {// 兼容模式判断
            $path_sep = '?' . Config::get('var_pathinfo') . '=';
        }
        $url = $domain . rtrim(self::$root ?: Request::instance()->root(), '/') . $path_sep . ltrim($url, '/');

Router.php

我们上节利用反射也看到这个$RTR就是CI_Router这个类,也看到相关的控制器和方法都是在路由对象$RTR上取的,所以在构造方法中应该解析了一切,我们的目的只有一个就是弄清楚class和method是怎么解析的?进入Router.php观察构造方法。
首先看到构造方法有个$routing参数,下面的注释说的很明白了,它是在index.php中设置并用来重写路由的,什么意思?就是不管你是那种路由,都将你隐射到同一个地方去,这个$routing是在index.php定义的中,这是CI提供的一个可选的方案,所以它是被注释掉的;

图片 1

在index.php中$routing的设置被注释掉了

接着往下看到,加载了config对象和Url对象,接着判断url是否启用了查询字符串模式,然后调用set_routing方法

图片 2

构造方法

进入set_routing方法,该方法的逻辑由两部分构成,查询字符串格式的路由解析和pathinfo格式的路由解析

图片 3

image.png

pathinfo
(PHP 4 >= 4.0.3, PHP 5)
pathinfo -- 返回文件路径的信息
说明
array pathinfo ( string path [, int options] )
pathinfo() 返回一个关联数组包含有 path 的信息。包括以下的数组单元:dirname,basename 和 extension。
可以通过参数 options 指定要返回哪些单元。它们包括:PATHINFO_DIRNAME,PATHINFO_BASENAME 和 PATHINFO_EXTENSION。默认是返回全部的单元。
例子 1. pathinfo() 例子
<?php
$path_parts = pathinfo("/www/htdocs/index.html");
echo $path_parts["dirname"] . "n";
echo $path_parts["basename"] . "n";
echo $path_parts["extension"] . "n";
?>
上例将输出:
/www/htdocs
index.html
html

查询字符串格式的路由解析

这块是查询字符串风格的url路由解析的最核心的地方了,并且终于能够解答我们一开始的疑问class和method到底是怎么来的?看代码

/*
通过$this->enable_query_strings为true进入查询字符串路由解析,我们前面说了这种url的风格为: 
example.com?c=test&m=index&param=param_value,
接下来通过读取配置directory_trigger值来判断url中是否指定了要访问的目录,
如果设置了就将其中的一些非打印字符给去掉,如果指定了访问的目录,那么就通过set_directory方法设置目录
*/
if ($this->enable_query_strings)
{
    $_d = $this->config->item('directory_trigger');
    $_d = isset($_GET[$_d]) ? trim($_GET[$_d], " tnrx0B/") : '';
    if ($_d !== '')
    {
    $this->uri->filter_uri($_d); //过滤非法字符
    $this->set_directory($_d);
    }

    /*
     接着读取控制器字段的配置值,通过该字段值读取url中要访问的控制器,终于看到了class和method是怎么解析得到的了
     */
    $_c = trim($this->config->item('controller_trigger'));
    if ( ! empty($_GET[$_c]))   //url中的控制器是否存在
    {
                $this->uri->filter_uri($_GET[$_c]);  //和上面一样,校验是否有非法字符
                /*
                  如果从url中读取的控制器类不为控制,那么就设置我们要访问的类,
                  原来$TRT->class读的class就是在这个方法中设置的
                */              
                $this->set_class($_GET[$_c]);  //set_class和set_method下文有截图

                $_f = trim($this->config->item('function_trigger'));//获取方法字段的配置值
                if ( ! empty($_GET[$_f]))
                {
                    $this->uri->filter_uri($_GET[$_f]); //校验非法字符
                    /*
                       如果从url中读取的控制器类不为控制,那么就设置我们要访问的类,
                       原来$TRT->method读的method就是在这个方法中设置的
                    */  
                    $this->set_method($_GET[$_f]);  
                }

                /*
                | 这一块是路由解析中非常重要的一段逻辑,在后面的分析中,我们始终看到凡是
                | 跟路由解析中url对象相关联的代码,其始终都在维护这样一个片段数组,第一个元素是控制器类,第二个是方法
                */
                //在url对象中存储相关的类和方法 
                $this->uri->rsegments = array(
                    1 => $this->class,
                    2 => $this->method
                );
    }
    else  //如果url中控制器是空的,那就设置默认的控制器类和方法
    {

                $this->_set_default_controller();
    }
    return;
}

关于_set_default_controller()方法设置默认控制器的逻辑有必要看下,因为当用户只是输入域名的情况总得有所处理吧,进入_set_default_controller方法看下

图片 4

_set_default_controller()

上面的代码中set_directory(),set_class(),set_method()方法体如下图

//设置url映射的控制器类
public function set_class($class)
    {
        $this->class = str_replace(array('/', '.'), '', $class);
    }

//设置url映射的方法
public function set_method($method)
    {
        $this->method = $method;
    }

//设置要加载控制器类的目录
public function set_directory($dir, $append = FALSE)
{
    if ($append !== TRUE OR empty($this->directory))
    {
        $this->directory = str_replace('.', '', trim($dir, '/')).'/';
    }
    else
    {
        $this->directory .= str_replace('.', '', trim($dir, '/')).'/';
    }
}

查询字符串风格的路由解析就到这里了,接下来看看pathinfo风格的。

我去写这段的时候在我的机器上没有输出任何内容,后来我在php安装目录下找到php.ini开启了cgi.fix_pathinfo=1,然后确实能够输出上面的内容。

pathinfo风格的路由解析

看代码

         /*
             如果存在路由配置文件,加载路由配置和特定环境(例如有些url可能只在测试环境能访问)的路由配置
         */
        if (file_exists(APPPATH.'config/routes.php'))
        {
            include(APPPATH.'config/routes.php');
        }

        if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/routes.php'))
        {
            include(APPPATH.'config/'.ENVIRONMENT.'/routes.php');
        }

        /*
                如果路由配置存在,获取到相应的默认控制器和url短线转换的标识,然后从路由数组中删除这两个,最后将路由数组赋值给routes变量,
                关于translate_uri_dashes见:   
                 https://codeigniter.org.cn/user_guide/general/routing.html?highlight=translate_uri_dashes#id6

        */
        if (isset($route) && is_array($route))
        {
            isset($route['default_controller']) && $this->default_controller = $route['default_controller'];
            isset($route['translate_uri_dashes']) && $this->translate_uri_dashes = $route['translate_uri_dashes'];
            unset($route['default_controller'], $route['translate_uri_dashes']);
            $this->routes = $route;
        }

        /* 接下来根据uri_string是否为空来决定走配置配置文件或者默认路由*/
        if ($this->uri->uri_string !== '')
        {
            $this->_parse_routes();
        }
        else
        {
            $this->_set_default_controller();
        }

上面的代码最后一段逻辑中看到$this->uri->uri_string,该属性到底是什么?因为后文的路由解析和此变量关联很大,我们先不要往下分析parse_toutes()方法了,进入url对象对应的文件URI.php中看下uri_string是什么?

但是这只是讲了一个函数的用法,就是pathinfo这个函数会返回一个数组,里面有三个元素分别是

URI.php

进入后观察构造方法

public function __construct()
{
         //加载配置对象用来读配置信息 

        $this->config =& load_class('Config', 'core');

        // 只处理来自命令行请求或者pathinfo风格的url请求
        if (is_cli() OR $this->config->item('enable_query_strings') !== TRUE)
        {
            $this->_permitted_uri_chars = $this->config->item('permitted_uri_chars');


            if (is_cli())  //解析命令行的url请求
            {
                $uri = $this->_parse_argv();
            } 
            else  //pathinfo格式的请求
            {

                /*
                  这里读取uri_protocol,并且我们可以在配置文件中看到uri_protocol 的默认值是REQUEST_URI,
                接下来根据uri_protocol的类型在switch...case从句来决定使用哪种解析方式, 总之就两种解析uri的
                方式         
                */

                $protocol = $this->config->item('uri_protocol');
                empty($protocol) && $protocol = 'REQUEST_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;
                }
            }

            //当url解析完成后,调用_set_uri_string($uri),我们猜这里会不会设置了$this->uri->uri_string?
            $this->_set_uri_string($uri);
        }

        log_message('info', 'URI Class Initialized');
    }

这段代码我们分析完了,但还是有两个疑问

第一个疑问,_parse_request_uri()和_parse_query_string()返回的变量$uri是什么,这两个方法有什么区别?

_parse_request_uri()方法

protected function _parse_request_uri()
{
        //判断浏览器域名的原生url和脚本名是否存在,如果不存在就返回一个空$uri,本质上是判断用户是否只是访问了我们的网站域名
        if ( ! isset($_SERVER['REQUEST_URI'], $_SERVER['SCRIPT_NAME']))
        {
            return '';
        }


        //如果是带有url访问,那么解析这个url,得到查询字符串和相关的path
        $uri = parse_url($_SERVER['REQUEST_URI']);
        $query = isset($uri['query']) ? $uri['query'] : '';
        $uri = isset($uri['path']) ? $uri['path'] : '';


        /*解析处理类似example.com/index.php/Test/index这种携带有入口文件的url,本质上是去掉REQUEST_URI中的SCRIPT_NAME,
         注意:REQUEST_URI是浏览器原生url,也就是域名后面的url段*/
        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'])));
            }
        }

        /*解析处理类似example.com/index.php/?/Test/index?name=tcl这种path位于查询字符串的url,
              解析得到uripath和查询字符串,并将原生的查询字符串重写*/
        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;
        }

        /*
         将原生查询字符串解析并设置到当前作用域,一旦解析到当前作用域,就可以使用php内置超全局预定义变量访问,
         此时解析原生查询字符串,我们就可以通过超全局预定义变量$_GET[]来获取查询字符串的值,关于超全局预定义变量见:
         http://php.net/manual/zh/language.variables.superglobals.php
        */
        parse_str($_SERVER['QUERY_STRING'], $_GET);

         //用户访问的只是根域名就返回  
        if ($uri === '/' OR $uri === '')
        {
            return '/';
        }

        //最后对path做一个清洗,因为url的格式可能为example.com/index.php/.././Test/index这种含有相对路径或绝对路径的url
        return $this->_remove_relative_directory($uri);
    }

_parse_query_string()方法

protected function _parse_query_string()
    {
        //检测是否只是访问了域名
        $uri = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : @getenv('QUERY_STRING');
        if (trim($uri, '/') === '')
        {
            return '';
        }
        //检测path是否位于查询字符串中,例如example.com/index.php/?/Test/index?name=tcl
        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);

       //对url做清洗
        return $this->_remove_relative_directory($uri);
    }

上述两个方法都用了_remove_relative_directory()对url中的相对路径做清洗,看下此方法

/*
 * 由于url可能是example.com/test../test./other/这种,
 * 此函数就是用来去除url中的相对路径
 *
 * */
protected function _remove_relative_directory($uri)
{
    $uris = array();
    //strtok和str_split类似也都是切割字符串,
    //不同处在于strtok会在第一次切割之后记住切割字符的位置,
    //之后就不再需要传原字符串了
    $tok = strtok($uri, '/');
    while ($tok !== FALSE)
    {
        if (( ! empty($tok) OR $tok === '0') && $tok !== '..')
        {
            $uris[] = $tok;
        }
        $tok = strtok('/');
    }

    return implode('/', $uris);
}

通过对_parse_request_uri()和_parse_query_string()这两个方法分析,我们看到_parse_query_string其实是对uri_protocol不支持REQUEST_URI的降级处理而已,这两个方法都返回了url中的path部分,这样我们就知道原来构造方法中的变量$uri就是url中path,该变量作为参数传递给了$this->_set_uri_string($uri)这个方法。

第二个疑问,$this->_set_uri_string($uri)这个方法会不会设置了我们解析pathinfo风格路由时看到的$this->uri->uri_string这个属性?
_set_uri_string()方法

protected function _set_uri_string($str)
    {
           //很显然uri_string这个属性是在这被设置的,它只是移除了一些非打印字符的$uri变量而已,
           //很清楚了,uri_string就是url中path部分
          $this->uri_string = trim(remove_invisible_characters($str, FALSE), '/');
        /*
          判断path是否为空,为空的$this->uri_string会导致路由解析走默认控制器,这个我们在解读_parse_routes()方法时会看到
        */
        if ($this->uri_string !== '')
        {
            //由于我们的url可能是example.com/Test/test.html这种静态页面,要想解析到正确的path就需要我们把后缀名去掉
            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);
                }
            }

           /*
              将path分割依然维护一个片段数组,第一个元素是控制器类,第二个是方法
            */
            $this->segments[0] = NULL;
            foreach (explode('/', trim($this->uri_string, '/')) as $val)
            {
                $val = trim($val);
                // 和之前一样,校验是否有非法字符
                $this->filter_uri($val);

                if ($val !== '')
                {
                    $this->segments[] = $val;
                }
            }
            unset($this->segments[0]);
        }
    }

通过解读该方法,我们看到其也维护了一个url片段数组,并且$this->uri->uri_string这个属性就是在这设置的,$this->uri->uri_string就是url中的path部分。

此外,我们在前文的分析中分析遇到了几个工具被频繁使用的工具函数,我们有必要看下它们
第一个,移除非打印字符的的函数remove_invisible_characters()

/*
     * 此函数主要移除ascii码表中非打印字符,
     * 包含0-31,以及127的删除符
     *
     * */
    function remove_invisible_characters($str, $url_encoded = TRUE)
    {
        $non_displayables = array();

        //由于回车r(13),换行n(10),制表t(9)这些用于文本格式的字符不需要过滤,
        //所以下面的处理排除了这三个字符

        if ($url_encoded)
        {
            $non_displayables[] = '/%0[0-8bcef]/';  // url encoded 00-08, 11, 12, 14, 15
            $non_displayables[] = '/%1[0-9a-f]/';   // url encoded 16-31
        }

        $non_displayables[] = '/[x00-x08x0Bx0Cx0E-x1Fx7F]+/S';   // 00-08, 11, 12, 14-31, 127
        do
        {
            $str = preg_replace($non_displayables, '', $str, -1, $count);
        }
        while ($count);

        return $str;
    }

ascii码表非打印字符见这里

换行符"n"和回车符"r",回车符应该确切来说叫做回车换行符
● 换行符就是另起一行 --- "n" 10 换行(newline)
● 回车符就是回到一行的开头 --- "r" 13 回车(return)
● Windows系统里面,每行结尾是回车+换行(CR+LF),即"rn";
● Unix系统里,每行结尾只有换行LF,即"n";
● Mac系统里,每行结尾是回车CR 即"r"。

第二个,过滤恶意字符的函数filter_uri()

/*
 * 这个函数主要过滤一些恶意的字符,因为恶意的攻击者可能从url中发起xss攻击,
 * 所以CI在配置文件config.php中的permitted_uri_chars指定了允许url允许的字符,
 * 一旦检测到恶意字符就报404
 * 
 * permitted_uri_chars本身是一段正则
 * $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对象(CI_URI)的解读就完成了,虽说比较繁琐,但总的来看,URI对象就做了两件事,第一是在访问的url中解析出path,这个path将会用在路由配置中查找真正映射的控制器和方法,第二是在path的基础上维护一个片段数组,这个数组存储了url中的类和方法

dirname:我的理解,服务器名称加上路径,
basename:访问的文件名,
extension:文件的扩展名

继续pathinfo风格的路由解析

现在我们知道上文中pathinfo风格的路出解析中我们看到的代码最后面的$this->uri->uri_string原来就是url中的path部分

        if ($this->uri->uri_string !== '')
        {
            $this->_parse_routes();
        }
        else
        {
            $this->_set_default_controller();
        }

既然得到了path,那就去路由配置文件中匹配真正映射的控制器类和方法,这一切在_parse_routes()方法中实现,在该方法中我们看到其处理了我们之前在CI框架的路由配置提到的四种配置方式

protected function _parse_routes()
    {
        //从url片段数组中组装出path,因为我们知道该数组保存了两个元素,一个是控制器类,另一个是方法
        $uri = implode('/', $this->uri->segments);

        // 这里得到请求方法是因为rest风格的路由配置中需要指明请求的动作,这将帮助我们匹配到rest路由
        $http_verb = isset($_SERVER['REQUEST_METHOD']) ? strtolower($_SERVER['REQUEST_METHOD']) : 'cli';

        /*
         * 这里匹配我们在配置文件中硬编码配置的路由和硬编码的rest风格的路由
         * */
        if (isset($this->routes[$uri]))
        {

            if (is_string($this->routes[$uri]))
            {
                $this->_set_request(explode('/', $this->routes[$uri]));
                return;
            }
            elseif (is_array($this->routes[$uri]) && isset($this->routes[$uri][$http_verb]))
            {
                $this->_set_request(explode('/', $this->routes[$uri][$http_verb]));
                return;
            }
        }

        /*
         * 这里匹配我们没有硬编码的动态路由,从CI框架的路由配置中知道,这几种动态路由为:
         * 通配符路由,正则路由,回调函数路由,这几种路由都转换成了正则路由然后进行了匹配
         * */
        foreach ($this->routes as $key => $val)
        {
            //检查是否为rest风格的动态路由,并获取真正的映射值
            if (is_array($val))
            {
                if (isset($val[$http_verb]))
                {
                    $val = $val[$http_verb];
                }
                else
                {
                    continue;
                }
            }

            //将通配符路由转换成正则路由
            $key = str_replace(array(':any', ':num'), array('[^/]+', '[0-9]+'), $key);

            //根据正则路由去匹配
            if (preg_match('#^'.$key.'$#', $uri, $matches))
            {
                /*
                 * 如果正则路由的映射值$val是回调函数,因为该函数的参数是正则表达式的子匹配,通过array_shift($matches)
                 * 来获取子匹配作为参数,然后通过call_user_func_array($val, $matches);实现对该回调函数的调用并返回真正的映射值
                 * */
                if ( ! is_string($val) && is_callable($val))
                {
                    array_shift($matches);

                    $val = call_user_func_array($val, $matches);
                }
                /* 由于在正则路由中,我们在映射部分还能使用逆向引用,例如$route['login/(.+)'] = 'auth/login/$1';
                那么我们就需要将path中配置到的这部分换成解析逆向引用后的映射值*/
                elseif (strpos($val, '$') !== FALSE && strpos($key, '(') !== FALSE)
                {
                    $val = preg_replace('#^'.$key.'$#', $val, $uri);
                }

                $this->_set_request(explode('/', $val));
                return;
            }
        }

        //如果走到这里还没有匹配到的话,只能将url片段传给_set_request做最后的处理
        $this->_set_request(array_values($this->uri->segments));
    }

在上面的_parse_routes()方法中我们看到从url片段数组中拿到path去路由配置文件去匹配真正的映射值,但是其中有个_set_request()方法是个什么鬼?我们看到其将真正映射值分割并传入其中,这个方法已经非常清楚了维护一个分割了真正映射值的url片段数组

protected function _set_request($segments = array())
    {

        /*
         * 因为即使我们拿到了真正映射的url片段数组,我们还是依然无法确保映射值对应的
         * 控制器类是否存在,这就需要调用_validate_request方法去验证下,并重置url片段数组
         * */
        $segments = $this->_validate_request($segments);
        if (empty($segments))
        {
            $this->_set_default_controller();
            return;
        }

        //短线转下划线,感觉这玩意很鸡肋,最好还是别设置为true
        if ($this->translate_uri_dashes === TRUE)
        {
            $segments[0] = str_replace('-', '_', $segments[0]);
            if (isset($segments[1]))
            {
                $segments[1] = str_replace('-', '_', $segments[1]);
            }
        }

        /*
         * 设置类和方法,这里有一点要注意,如果没有方法,将会以控制器类中的index方法作为默认,
         * 这也是为什么我们访问example.com/Test/ 时发现进入index方法的原因
         * */
        $this->set_class($segments[0]);
        if (isset($segments[1]))
        {
            $this->set_method($segments[1]);
        }
        else
        {
            $segments[1] = 'index';
        }

        //重置url的片段数组的索引
        array_unshift($segments, NULL);
        unset($segments[0]);
        $this->uri->rsegments = $segments;
    }

下面是_validate_request()方法

/*
 *从url片段数组中尝试去加载控制器类,
 * 一旦载入成功后设置目录,并重置url片段数组,
 * */
protected function _validate_request($segments)
{
    $c = count($segments);

    while ($c-- > 0)
    {
        $test = $this->directory
            .ucfirst($this->translate_uri_dashes === TRUE ? str_replace('-', '_', $segments[0]) : $segments[0]);

        if ( ! file_exists(APPPATH.'controllers/'.$test.'.php') && is_dir(APPPATH.'controllers/'.$this->directory.$segments[0]))
        {
            $this->set_directory(array_shift($segments), TRUE);
            continue;
        }

        return $segments;
    }

    //该数组存储了最终映射的控制器类和方法以及相关的参数
    return $segments;
}

下面用流程图理一下整个路由解析的过程

图片 5

image.png

至此整个路由对象对路由解析的代码解读就结束了,我们发现路由对象就做了四件事

  • 设置目录set_directory()
  • 设置控制器类set_class()
  • 设置类方法set_method()
  • 维护一个url片段数组。

服务器支持后还需要在配置文件中打开相应的配置,

// 开启路由
'URL_ROUTER_ON'   => true, 

本文由88必发手机版发布于仪器仪表,转载请注明出处:CodeIgniter源码分析,看不懂的路由规则

关键词:

上一篇:CodeIgniter源码分析,看不懂的路由规则
下一篇:没有了