CI框架源码解析九之路由类文件Router.php

本文深入讲解了CI框架中Router类的工作原理,包括自定义路由规则、支持回调函数、使用HTTP动词等功能。并通过代码示例介绍了如何配置和使用这些特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        本篇博文将讲述CI框架Router路由类文件,CI框架Router路由类将URI映射到对应的控制器及方法,Router类大量代码处理的是自定义路由,该类要支撑以下几个功能点:

① 自定义路由规则

        在 application/config/routes.php 文件中的 $route 的数组,利用它可以设置路由规则。 在路由规则中可以使用通配符或正则表达式。使用通配符:$route['product/:num'] = 'catalog/product_lookup';使用正则:$route['products/([a-z]+)/(\d+)'] = '$1/id_$2';匹配只含有数字的一段。 (:any) 匹配含有任意字符的一段。(除了 '/' 字符,因为它是段与段之间的分隔符)。

        通配符实际上是正则表达式的别名,:any 会被转换为 [^/]+ , :num 会被转换为 [0-9]+ 。$key = str_replace(array(':any', ':num'), array('[^/]+', '[0-9]+'), $key);

② 支持回调函数

        在路由规则中使用回调函数来处理逆向引用。 例如:

    $route['products/([a-zA-Z]+)/edit/(\d+)'] = function ($product_type, $id)
    {
        return 'catalog/product_edit/' . strtolower($product_type) . '/' . $id;
    };

③ 支持使用HTTP动词

        在路由数组后面再加一个键,键名为 HTTP 动词。可以使用标准的 HTTP 动词(GET、PUT、POST、DELETE、PATCH),也可以使用自定义的动词 (例如:PURGE),不区分大小写。例如:

    //发送 PUT 请求到 "products" 这个 URI 时,将会调用 Product::insert() 方法
    $route['products']['put'] = 'product/insert';
    
    //发送 DELETE 请求到第一段为 "products" ,第二段为数字这个 URL时,将会调用 Product::delete() 方法,并将数字作为第一个参数。
    $route['products/(:num)']['DELETE'] = 'product/delete/$1';

        先看一张Router工作的流程图:


        CI在CodeIgniter.php中实例化路由时,就完成了解析,得到请求的控制器名及方法名了。$RTR =& load_class("Router', 'core', isset($routing) ? $routing : NULL);所以核心入口是__construct()。

1、构造函数(__construct())

    public function __construct($routing = NULL)
    {
        //加载类内部的类
        $this->config =& load_class('Config', 'core');
        $this->uri =& load_class('URI', 'core');
    
        //确认是否开启querystirng模式,如果这个模式开启,那就用index.php?c=mall&a=list这样去访问控制器和方法了
        $this->enable_query_strings = (!is_cli() && $this->config->item('enable_query_strings') === TRUE);
        
        //如果在index.php里指定控制器目录,那么在动态路由之前都将这个设置作为控制器的目录
        //通俗的说就是路由器在找控制器和方法时,会在“contrlloer/设置的目录/”下找
        //而且这个设置会覆盖URI(三段)的目录
        is_array($routing) && isset($routing['directory']) && $this->set_directory($routing['directory']);
    
        //核心:解析URI到$this->directory、$this->class、$this->method
        $this->_set_routing();
        
        //如果在index.php中设置了控制器和方法,则覆盖
        //比如服务器维护时,设置一个方法用来显示“维护中”的静态页面,就可以让任何URI的请求都进入到该个方法中显示静态页面
        //我在想:应该把上面的$this->_set_routing();放到这个else块中就完美了
        if (is_array($routing)) {
            empty($routing['controller']) OR $this->set_class($routing['controller']);
            empty($routing['function']) OR $this->set_method($routing['function']);
        }
        log_message('info', 'Router Class Initialized');
    }

2、核心解析函数(_set_routing())

    protected function _set_routing()
    {
        //加载路由配置文件routes.php
        if (file_exists(APPPATH . 'config/routes.php')) {
            include(APPPATH . 'config/routes.php');
        }
        //如果有环境对应的配置文件,则加载并覆盖原配置文件routes.php
        if (file_exists(APPPATH . 'config/' . ENVIRONMENT . '/routes.php')) {
            include(APPPATH . 'config/' . ENVIRONMENT . '/routes.php');
        }
    
        //读取默认控制器设置$route['default_controller']
        //读取$route['translate_uri_dashes']。如果设置为TRUE,则可将URI中的破折号-转换成类名的下划线_
        //如my-controller/index    -> my_controller/index
        //读取所有自定义路由策略赋值给$this->routes
        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;
        }
        //在querystring模式下获取directory/class/method
        //index.php?d=admin&c=mall&m=list
        //$config['controller_trigger'] = 'c';//控制器变量
        //$config['function_trigger'] = 'm';//方法变量
        //$config['directory_trigger'] = 'd';//目录变量
        if ($this->enable_query_strings) {
            //获取$this->directory。配置文件中的'directory_trigger'代表在$_GET中用什么变量名作为传递directory的键值
            //同样的还有设置控制器的传递参数键名controller_trigger,方法的传递参数键名function_trigger
            if (!isset($this->directory)) {
                $_d = $this->config->item('directory_trigger');
                $_d = isset($_GET[$_d]) ? trim($_GET[$_d], " \t\n\r\0\x0B/") : '';
                if ($_d !== '') {
                    //filter_uri是验证uri的组成字符是否在白名单(配置文件中permitted_uri_chars设置)中
                    $this->uri->filter_uri($_d);
                    $this->set_directory($_d);
                }
            }
            //获取控制器和方法,并设置$this->uri->rsegments
            $_c = trim($this->config->item('controller_trigger'));
            if (!empty($_GET[$_c])) {
                $this->uri->filter_uri($_GET[$_c]);
                $this->set_class($_GET[$_c]);
                $_f = trim($this->config->item('function_trigger'));
                if (!empty($_GET[$_f])) {
                    $this->uri->filter_uri($_GET[$_f]);
                    $this->set_method($_GET[$_f]);
                }
                $this->uri->rsegments = array(
                    1 => $this->class,
                    2 => $this->method
                );
            } else {
                //方法没有可以允许,如果控制器都没有,就调用默认控制器和方法代替了
                $this->_set_default_controller();
            }
            return;
        }
        // 非querystring模式的程序可以走到这里
        if ($this->uri->uri_string !== '') {
            //解析自定义路由规则,并调用_set_request函数设置目录、控制器、方法
            $this->_parse_routes();
        } else {
            //uri_string为空,一般情况下就是域名后面没有任何字符,调用默认控制器
            $this->_set_default_controller();
        }
    }

        下面本来还想继续把自定义路由解析函数(_parse_routes())、设置目录、控制器和方法名、从uri片段中抽取目录等函数代码贴上来,一想最后要把文件的全部的代码都贴上来就不一段一段的贴了,并且要说的都写在代码注释里了,也没有什么文字就不不再贴了。

        最后,贴一下整个路由类Router.php文件的源码(注释版):

    <?php
    
    /**
     * =======================================
     * Created by Pocket Knife Technology.
     * User: ZhiHua_W
     * Date: 2016/10/25 0014
     * Time: 上午 9:27
     * Project: CodeIgniter框架—源码分析
     * Power: Analysis for Router.php
     * =======================================
     */
    
    //不说
    defined('BASEPATH') OR exit('No direct script access allowed');
    
    /**
     * Router类:将URI映射到对应的控制器及方法
     * Router类大量代码处理的是自定义路由,该类要支撑以下几个功能点:
     * 1、自定义路由规则
     * 2、支持回调函数
     * 3、支持使用 HTTP 动词
     */
    class CI_Router
    {
        public $config;
        public $routes = array();
        public $class = '';
        public $method = 'index';
        public $directory;
        public $default_controller;
        public $translate_uri_dashes = FALSE;
        public $enable_query_strings = FALSE;
    
        /**
         * 构造函数
         */
        public function __construct($routing = NULL)
        {
            //加载类内部的类
            $this->config =& load_class('Config', 'core');
            $this->uri =& load_class('URI', 'core');
            //确认是否开启querystirng模式,如果这个模式开启,那就用index.php?c=mall&a=list这样去访问控制器和方法了
            $this->enable_query_strings = (!is_cli() && $this->config->item('enable_query_strings') === TRUE);
    
            //如果在index.php里指定控制器目录,那么在动态路由之前都将这个设置作为控制器的目录
            //通俗的说就是路由器在找控制器和方法时,会在“contrlloer/设置的目录/”下找
            //而且这个设置会覆盖URI(三段)的目录
            is_array($routing) && isset($routing['directory']) && $this->set_directory($routing['directory']);
            //核心:解析URI到$this->directory、$this->class、$this->method
            $this->_set_routing();
    
            //如果在index.php中设置了控制器和方法,则覆盖
            //比如服务器维护时,设置一个方法用来显示“维护中”的静态页面,就可以让任何URI的请求都进入到该个方法中显示静态页面
            //我在想:应该把上面的$this->_set_routing();放到这个else块中就完美了
            if (is_array($routing)) {
                empty($routing['controller']) OR $this->set_class($routing['controller']);
                empty($routing['function']) OR $this->set_method($routing['function']);
            }
            log_message('info', 'Router Class Initialized');
        }
    
        protected function _set_routing()
        {
            //加载路由配置文件routes.php
            if (file_exists(APPPATH . 'config/routes.php')) {
                include(APPPATH . 'config/routes.php');
            }
            //如果有环境对应的配置文件,则加载并覆盖原配置文件routes.php
            if (file_exists(APPPATH . 'config/' . ENVIRONMENT . '/routes.php')) {
                include(APPPATH . 'config/' . ENVIRONMENT . '/routes.php');
            }
    
            //读取默认控制器设置$route['default_controller']
            //读取$route['translate_uri_dashes']。如果设置为TRUE,则可将URI中的破折号-转换成类名的下划线_
            //如my-controller/index    -> my_controller/index
            //读取所有自定义路由策略赋值给$this->routes
            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;
            }
    
            //在querystring模式下获取directory/class/method
            //index.php?d=admin&c=mall&m=list
            //$config['controller_trigger'] = 'c'; 控制器变量
            //$config['function_trigger'] = 'm'; 方法变量
            //$config['directory_trigger'] = 'd'; 目录变量
            if ($this->enable_query_strings) {
                //获取$this->directory。配置文件中的'directory_trigger'代表在$_GET中用什么变量名作为传递directory的键值
                //同样的还有设置控制器的传递参数键名controller_trigger,方法的传递参数键名function_trigger
                if (!isset($this->directory)) {
                    $_d = $this->config->item('directory_trigger');
                    $_d = isset($_GET[$_d]) ? trim($_GET[$_d], " \t\n\r\0\x0B/") : '';
                    if ($_d !== '') {
                        //filter_uri是验证uri的组成字符是否在白名单(配置文件中permitted_uri_chars设置)中
                        $this->uri->filter_uri($_d);
                        $this->set_directory($_d);
                    }
                }
                //获取控制器和方法,并设置$this->uri->rsegments
                $_c = trim($this->config->item('controller_trigger'));
                if (!empty($_GET[$_c])) {
                    $this->uri->filter_uri($_GET[$_c]);
                    $this->set_class($_GET[$_c]);
    
                    $_f = trim($this->config->item('function_trigger'));
                    if (!empty($_GET[$_f])) {
                        $this->uri->filter_uri($_GET[$_f]);
                        $this->set_method($_GET[$_f]);
                    }
    
                    $this->uri->rsegments = array(
                        1 => $this->class,
                        2 => $this->method
                    );
                } else {
                    //方法没有可以允许,如果控制器都没有,就调用默认控制器和方法代替了
                    $this->_set_default_controller();
                }
                return;
            }
            //非querystring模式的程序可以走到这里
            if ($this->uri->uri_string !== '') {
                //解析自定义路由规则,并调用_set_request函数设置目录、控制器、方法
                $this->_parse_routes();
            } else {
                //uri_string为空,一般情况下就是域名后面没有任何字符,调用默认控制器
                $this->_set_default_controller();
            }
        }
    
        protected function _set_request($segments = array())
        {
            /**
             * 看,这里有调用Router::_validate_request();而Router::_validate_request()的作用是检测寻找出一个
             * 正确存在的路由,并确定它,确定后的值分别放到Rouer::$class这些属性里面。所以使到这个_set_request()也有
             * 这种确定路由的功能。
             *
             * 注:
             * $segments=$this->_validate_request($segments); 等式右边,括号里面的这个$segments,也就是调用
             * _set_request()时传入来的这个参数,它有这样的特点:
             * 1)如果这时_set_request()是在Router::_set_default_controller()中调用的话,那个这个$segments是永远不会为
             *  空数组,嗯,绝对不会。
             *
             * 而左边这个$segments的值,经过下面这行代码后,要么为空数组array(),要么为确定路由后的段数组。
             * 为空数组的原因是,$this->_validate_request();里面没有找到当前目录的默认控制器。此时,右边的
             * $segments要么为空,要么只指定了目录但默认控制器不存在。
             */
            //从$segments中提取Directory信息,设置$this->directory
            $segments = $this->_validate_request($segments);
            // If we don't have any segments left - try the default controller;
            // WARNING: Directories get shifted out of the segments array!
            //如果$segments在目录被提取走后,没有剩下任何东西,那么就用默认路由
            if (empty($segments)) {
                //所以如果上面返回了空数组,就会进到这里。
                //这里居然又调回了_set_default_controller()! 坑爹吧!
                $this->_set_default_controller();
                return;
                /**
                 * 我曾经想过,下面这里会不会死循环:
                 * 假如,我在配置文件里面的默认控制器设为welcome,然后controllers/下没有welcome.php,但controllers/下有
                 * welcome/有这个目录(里面没东西),然后通过http://localhost/CI/来访问默认控制器,那会怎样呢?
                 * 首先,它会进入_set_routing();然后发现$this->uri->uri_string为空,进入_set_default_controller();
                 * 然后发现在_set_default_controller里,发现$this->default_controller不为FALSE,(@@@@),然后再
                 * 进入这_set_request()里面,再进入_validate_request()里面,会不会_validate_request里返回空数组?因为
                 * 指定了目录,没有指定控制器,访问默认的,又不存在,然后返回空数组,返回空数组后,最终就会走来你正在看的这个位置,
                 * 然后这个位置再调用_set_default_controller();然后死循环了。。。
                 *
                 * 答案是不会的。
                 * 原因在于:
                 * 我们回到上面解译那个(@@@@)的地方,在这里,发现$this->default_controller不为FALSE后,它会进入这个else
                 * 里面
                 * else
                 * {
                 *  $this->set_class($this->default_controller);   ..............1
                 *  $this->set_method('index');                 ...................2
                 *  $this->_set_request(array($this->default_controller, 'index'));  ..........3
                 * }
                 *
                 * 然后第3行,传入_set_request($segments)中的那个$segments其实是
                 * array('welcome','index'),重点在于那个小小的'index'!!!!!!!
                 * 这样一来,我们进入_validate_request()的时候,我们实质并没有“指定目录但没有指定控制器,访问默认控制器”,
                 * 而是“指定了一个welcome的目录,和一个叫index的控制器!!”,所以才不会死循环。
                 * 如果你试着把第3行那个'index'去掉,那么,一定会死循环!!!!!!!!不信试试!CI太牛逼了,居然这样做。汗。。
                 * 当然,‘index’还有一个作用,就是设置默认方法啦。
                 */
            }
    
            //如果允许路径中破折号存在,也就是路径中破折号'-'映到至类名的下划线 '_'
            if ($this->translate_uri_dashes === TRUE) {
                $segments[0] = str_replace('-', '_', $segments[0]);
                if (isset($segments[1])) {
                    $segments[1] = str_replace('-', '_', $segments[1]);
                }
            }
            //设置控制器类
            $this->set_class($segments[0]);
            if (isset($segments[1])) {
                //设置控制器类方法
                $this->set_method($segments[1]);
            } else {
                //如果不存在方法片段,则默认方法名为index
                $segments[1] = 'index';
            }
            //这里要说一下,现在是在ROUTER里面为URI赋值,URI里面的这个URI::$rsegments是经过处理,并确定路由后,实质调用的路由的段信息。
            //而URI::$segments (前面少了个r),则是原来没处理前的那个,即直接由网址上面得出来的那个。
            //将整个数组元素往后推一格,保持和没有shift掉目录时的数组原素存放序列一致,
            //如array ( 0 => 'news', 1 => 'view', 2 => 'crm', )经过这两行后变成array ( 1 => 'news', 2 => 'view', 3 => 'crm', )
            //不过要是多级目录的话,这样推有什么用呢?
            array_unshift($segments, NULL);
            unset($segments[0]);
            //RTR->uri->rsegments用来存放路由转换后的片段,不含目录
            $this->uri->rsegments = $segments;
        }
    
        protected function _set_default_controller()
        {
            //在Router::_set_routing()函数里面有一个操作,是从配置文件里面读取默认控制器名
            if (empty($this->default_controller)) {
                //如果没有默认的话,就报错,结束程序。
                //实质上,这个_set_default_controller()仅仅是在uri没有指定控制器,要求访问默认控制器的时候才
                //被调用,所以如果连默认控制器都没有,那么可以果断报错。
                show_error('Unable to determine what should be displayed. A default route has not been specified in the routing file.');
            }
            //如果有,下面我们就来把默认的控制器设置为当前要找的路由。
    
            //这里只是分“有指定默认方法”和“没有指定”两种情况而已。不过要弄点下面那个$this->_set_request($x);
            //CI这几个函数也许写得很妙,但是让人看得纠结。
            if (sscanf($this->default_controller, '%[^/]/%s', $class, $method) !== 2) {
                $method = 'index';
            }
            if (!file_exists(APPPATH . 'controllers/' . $this->directory . ucfirst($class) . '.php')) {
                return;
            }
            $this->set_class($class);
            $this->set_method($method);
            $this->uri->rsegments = array(
                1 => $class,
                2 => $method
            );
            log_message('debug', 'No URI present. Default controller set.');
        }
    
        protected function _validate_request($segments)
        {
            $c = count($segments);
            $directory_override = isset($this->directory);
            //支持多级目录
            while ($c-- > 0) {
                $test = $this->directory . ucfirst($this->translate_uri_dashes === TRUE ? str_replace('-', '_', $segments[0]) : $segments[0]);
                //如果直接在controllers这个目录下找到与第一段相应的控制器名,那就说明找到了控制器,确定路由,返回。
                if (!file_exists(APPPATH . 'controllers/' . $test . '.php') && $directory_override === FALSE && is_dir(APPPATH . 'controllers/' . $this->directory . $segments[0])) {
                    //如果的确是目录,那么就可以确定路由的目录部分了。
                    $this->set_directory(array_shift($segments), TRUE);
                    continue;
                }
                //如果上面没有找到,再看看这个“第一段”是不是一个目录,因为CI是允许控制器放在自定义的目录下的。
                return $segments;
            }
            return $segments;
        }
    
        protected function _parse_routes()
        {
            //知道_set_request()是干嘛的之后,下面的条理就比较清晰了。
            $uri = implode('/', $this->uri->segments);
            $http_verb = isset($_SERVER['REQUEST_METHOD']) ? strtolower($_SERVER['REQUEST_METHOD']) : 'cli';
            //CI有路由重定向的功能,重定向的规则和实现就是在这里。
            foreach ($this->routes as $key => $val) {
                if (is_array($val)) {
                    $val = array_change_key_case($val, CASE_LOWER);
                    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)) {
                    //利用回调过程反向引用
                    if (!is_string($val) && is_callable($val)) {
                        //从匹配数组中删除原始字符串
                        array_shift($matches);
                        //使用在匹配中的值执行回调函数作为参数
                        $val = call_user_func_array($val, $matches);
                    } elseif (strpos($val, '$') !== FALSE && strpos($key, '(') !== FALSE) {
                        $val = preg_replace('#^' . $key . '$#', $val, $uri);
                    }
                    $this->_set_request(explode('/', $val));
                    return;
                }
            }
            //匹配的路线,所以我们将设置网站默认路由
            $this->_set_request(array_values($this->uri->segments));
        }
    
        //设置类
        public function set_class($class)
        {
            $this->class = str_replace(array('/', '.'), '', $class);
        }
    
        //获取当前类
        public function fetch_class()
        {
            return $this->class;
        }
    
        //设置方法名
        public function set_method($method)
        {
            $this->method = $method;
        }
    
        //获取当前方法
        public function fetch_method()
        {
            return $this->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, '/')) . '/';
            }
        }
    
        //获取目录
        public function fetch_directory()
        {
            return $this->directory;
        }
    
    }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值