路由类Router.php

路由是MVC框架的核心内容,路由的作用是寻找和用户的请求URL相匹配的类和方法[处理器],也可以说是请求分发器,将用户的请求分发到对应的文件进行处理。 进行路由解析之前需要对用户的请求URI进行预处理,这部分内容主要是在地址解析中介绍。我们应该也了解CodeIgniter对不同风格的URI的支持,和地址解析类似针对不同风格的URI,路由规则也不同。总体而言,CodeIgniter的路由工作是URI类和Router类配合完成的,针对不同的配置进行不同URL风格进行预处理并采用不同的路由规则进行解析。

用户配置

CodeIgniter的路由和地址解析类时相互配合完成工作的,在不同模式下的Router类的路由规则也略有不同的,同样依赖于config.php中的配置。

  • $config['enable_query_string']=True时,路由解析是工作在QUERY_STRING的模式下,路由解析类直接从查询字符串中寻找类和方法。需要注意的是这种情况下用户定义的路由规则是不生效的,这时候路由解析都是基于查询字符串完成的
$config['controller_trigger'] = 'c';
$config['function_trigger'] = 'm';
$config['directory_trigger'] = 'd';

以上3个配置项也是定义在config.php中的,他们分别的定义了URI中查询字符串中定义的控制器,方法和文件目录的缩写。

  • $config['enable_query_string']=False时,路由解析工作在REQUEST_URI的模式下,这时候需要进行路由规则的匹配,路由的优先级为用户定义路由规则->默认路由规则->默认控制器/方法->404错误。用户自定义的路由规则在 application/config/routes.php 文件中,这个文件定义了一个$route数组。

CodeIgniter中的自定义路由规则比较强大,能够支持通配符的形式、支持正则表达式的形式,支持设置自定义的回调函数来处理逆向引用,支持使用HTTP动词来进行更精准的路由匹配,具体的定义格式等内容可以参见CI用户手册的URI路由章节,有助于理解Router类是如何分别支持以上自定义规则的。

另外还需要指出的是,CodeIgniter中保留了以下几条默认路由规则。

$route['default_controller'] = 'welcome';
$route['404_override'] = '';
$route['translate_uri_dashes'] = FALSE;

属性概览

Router类的属性并不多,根据名称也可以基本了解其对应的含义。比较重要的是路由解析之后的结果保存在属性$class和$method中,$directory保存着请求对应的类文件的目录。

属性名称 注释
public $config; 配置类CI_CONFIG的对象
public $routes = array(); 获取并保存从配置文件中得到的路由规则
public $class = ''; 当前的类名称
public $method = 'index'; 当前的方法名称,默认为index
public $directory; 包含请求的类和方法的次级目录
public $default_controller; 默认的控制器类名称
public $translate_uri_dashes = FALSE; 确定URI中的“-”是否需要转化成“_”的标志位
public $enable_query_strings = FALSE; 是否开启query_string的标志位

方法概览

Router类的方法也不多,除了用于解析URI的方法之外还有几组get和set方法,这些方法在框架中适用于主文件中存在覆盖的情况,当然用户也可以调用来覆写某个请求的实际控制类和方法。下面整理进行路由解析的几个函数之间的调用关系。

方法名称 注释
__construct() 构造函数
_set_routing() 进行路由解析并保存结果
_set_request($segments = array()) 根据默认规则匹配路由
_set_default_controller() 设置默认的controller
_validate_request($segments) 验证uri并识别目录
_parse_routes() request_uri模式下路由解析
set_class($class) 设置用户请求对应的class
fetch_class() 获取和返回类的$class属性
set_method($method) 设置用户请求对应的method
fetch_method() 获取和返回类的$method属性
set_directory($dir, $append = FALSE) 设置用户请求对应的directory
fetch_directory() 获取和返回类的$directory属性

构造函数__construct()

构造函数调用了_set_routing()函数完成了路由解析的过程,并且将结果保存在类的属性$class和$method中,同时考虑到主文件中覆盖解析结果的情况

public function __construct($routing = NULL)
{
   $this->config =& load_class('Config', 'core');
   //魔术方法,拦截器把URI对象作为路由类的属性
   $this->uri =& load_class('URI', 'core');
   //设置属性enable_query_strings的值
   $this->enable_query_strings = ( ! is_cli() && $this->config->item('enable_query_strings') === TRUE);

   //如果设置了$routing且包含目录,需要在动态路由之前设置该目录
   is_array($routing) && isset($routing['directory']) && $this->set_directory($routing['directory']);
   //加载config/route.php文件,完成query_string或request_uri模式下的路由解析
   //将路由解析的结果保存在Router类的属性class和method
   $this->_set_routing();

   //如果$routing中定义了controller和function表示覆盖,则直接覆写class和method
   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');
}

进行路由解析_set_routing()

_set_routing()是进行路由解析的核心方法,从加载用户自定义的路由规则开始,到针对query_string和request_uri两种设置进行路由解析并保存最终的结果,query_string设置下直接根据查询字符串解析进行,解析失败会调用设置默认控制器函数_set_default_controller(),request_uri设置下的解析则调用函数_parse_routes()进行处理。

protected function _set_routing()
{
   //加载routes.php文件
   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');
   }

   //验证和设置默认controller,破折号下划线的值,并将剩下的路由规则保存在$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;
   }

   //如果启用query_strings模式,寻找class/function的规则处理如下
   if ($this->enable_query_strings)
   {
      //如果已经设置了directory表示出现覆盖跳过directory的获取,否则获取方法如下
      if ( ! isset($this->directory))
      {
         //获取配置项中directory的缩写,并从$_GET中取得,过滤,赋值给directory
         $_d = $this->config->item('directory_trigger');
         $_d = isset($_GET[$_d]) ? trim($_GET[$_d], " \t\n\r\0\x0B/") : '';

         if ($_d !== '')
         {
            $this->uri->filter_uri($_d);
            $this->set_directory($_d);
         }
      }

      $_c = trim($this->config->item('controller_trigger'));
      if ( ! empty($_GET[$_c]))
      {
         //分别根据请求字符串设置class和function,并将其保存在URI对象的rsegments数组中
         $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
      {
          //设置默认的controller
         $this->_set_default_controller();
      }
      //路由规则在query_string模式下不适用,且不用定位目录,所以到这里就完成了路由
      return;
   }
   //如果经过URI对象处理后的uri_string不为空,就需要路由处理;否则设置默认的controller
   if ($this->uri->uri_string !== '')
   {
       //querst_uri模式下的路由解析方法
      $this->_parse_routes();
   }
   else
   {
      $this->_set_default_controller();
   }
}

REQUEST_URI模式下的URI解析_parse_routes()

_parse_routes()是request_uri设置下的路由解析方法,分别针对用户自定义的路由规则进行处理,优先处理HTTP动词,然后将通配符替换为正则进行匹配,处理回调函数和逆向引用的情况,如果都没有匹配成功,则根据默认的路由规则调用_set_request()函数进行处理。

protected function _parse_routes()
{
   //将URI对象的segments数组转化成uri字符
   $uri = implode('/', $this->uri->segments);

   //获取HTTP动词
   $http_verb = isset($_SERVER['REQUEST_METHOD']) ? strtolower($_SERVER['REQUEST_METHOD']) : 'cli';

   //从路由规则中寻找匹配的规则
   foreach ($this->routes as $key => $val)
   {
      //如果定义了HTTP动词,那一定是一个二维数组
      //这种情况严格匹配HTTP动词,匹配失败直接跳过当前$ket匹配下个$key
      if (is_array($val))
      {
         $val = array_change_key_case($val, CASE_LOWER);
         if (isset($val[$http_verb]))
         {
            //从多个HTTP动词中选择出匹配的哪个作为$val,还要继续匹配$key
            $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))
         {
            //首元素保存的是controller名称
            array_shift($matches);

            //用匹配上的数据作为回调函数的参数并执行
            $val = call_user_func_array($val, $matches);
         }
         //用于处理参数的逆向引用
         elseif (strpos($val, '$') !== FALSE && strpos($key, '(') !== FALSE)
         {
            $val = preg_replace('#^'.$key.'$#', $val, $uri);
         }
         //用于处理常规的uri路由规则
         $this->_set_request(explode('/', $val));
         return;
      }
   }
   //如果走到这一步,表示用户路由规则中没有匹配成功,根据默认规则匹配路由
   $this->_set_request(array_values($this->uri->segments));
}

解析uri中文件目录_validate_request($segments)

处理URI类处理之后的$segments,按顺序匹配直到找到控制器文件为止,并把之前的数组元素设置在目录中。

protected function _validate_request($segments)
{
   $c = count($segments);
   $directory_override = isset($this->directory);

   //$segments中保存的应该是dir名称+class名称+method名称
       //从数组首位开始循环知道找到第一个不是directory的元素或者找到controller文件终止
   while ($c-- > 0)
   {
      $test = $this->directory
         .ucfirst($this->translate_uri_dashes === TRUE ? str_replace('-', '_', $segments[0]) : $segments[0]);
           //当前元素是dir名称且没有对应的controller文件
      if ( ! file_exists(APPPATH.'controllers/'.$test.'.php')
         && $directory_override === FALSE
         && is_dir(APPPATH.'controllers/'.$this->directory.$segments[0])
      )
      {
          //把当前的$segments的首元素拼接到directory中
         $this->set_directory(array_shift($segments), TRUE);
         continue;
      }
           //到这一步表示返回的$segments的首元素不是dir目录或者是controller文件
      return $segments;
   }

   //到这一步返回的已经是空数组了,所有元素表示的都是目录
   return $segments;
}

默认规则解析控制器和方法_set_request($segments = array())

_set_request()函数处理的是_validate_request()处理之后剩余的$segments,如果为空表示没有对应的控制器,设置默认控制器,否则表示找到了控制器文件,保存结果即可。

protected function _set_request($segments = array())
{
   //验证uri并将其中的dir元素移除并拼接到类的$directory属性中
   $segments = $this->_validate_request($segments);

   //如果已经没有元素就只能设置默认的controller了
   if (empty($segments))
   {
      $this->_set_default_controller();
      return;
   }

   //把uri中的"-"转化成"_"
   if ($this->translate_uri_dashes === TRUE)
   {
      $segments[0] = str_replace('-', '_', $segments[0]);
      if (isset($segments[1]))
      {
         $segments[1] = str_replace('-', '_', $segments[1]);
      }
   }

   //如果$segments[1]存在那么设置其为方法名,否则默认设置index
   $this->set_class($segments[0]);
   if (isset($segments[1]))
   {
      $this->set_method($segments[1]);
   }
   else
   {
      $segments[1] = 'index';
   }
   //保证$segments数组有用下标是从1开始的
   array_unshift($segments, NULL);
   unset($segments[0]);
   $this->uri->rsegments = $segments;
}

设置默认的控制器和方法_set_default_controller()

顾名思义,也很容易理解,设置默认的控制器和方法。

protected function _set_default_controller()
{
    //default_controller是保留路由,不设置这里会报错
   if (empty($this->default_controller))
   {
      show_error('Unable to determine what should be displayed. A default route has not been specified in the routing file.');
   }
   //如果没有设置方法,将其设置为index
   if (sscanf($this->default_controller, '%[^/]/%s', $class, $method) !== 2)
   {
      $method = 'index';
   }
   //寻找$class首字母大写的php文件失败
   if ( ! file_exists(APPPATH.'controllers/'.$this->directory.ucfirst($class).'.php'))
   {
      //这里之后会触发404错误
      return;
   }
   //设置这种情况下路由到的的class和method
   $this->set_class($class);
   $this->set_method($method);
   //把路由结果保存到uri对象的rsegments数组中,下标从1开始
   $this->uri->rsegments = array(
      1 => $class,
      2 => $method
   );
   log_message('debug', 'No URI present. Default controller set.');
}

书籍推荐