本篇博文将讲述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;
}
}