Router类:将URI映射到对应的控制器及方法
Router类大量代码处理的是自定义路由,该类要支撑以下几个功能点:
1、自定义路由规则
在 application/config/routes.php 文件中的 $route 的数组,利用它可以设置路由规则。 在路由规则中可以使用通配符或正则表达式。
使用通配符:$route['product/:num'] = 'catalog/product_lookup';
使用正则:$route['products/([a-z]+)/(\d+)'] = '$1/id_$2';
(:num) 匹配只含有数字的一段。 (:any) 匹配含有任意字符的一段。(除了 '/' 字符,因为它是段与段之间的分隔符)
通配符实际上是正则表达式的别名,:any 会被转换为 [^/]+ , :num 会被转换为 [0-9]+ 。
$key = str_replace(array(':any', ':num'), array('[^/]+', '[0-9]+'), $key);
2、支持回调函数
在路由规则中使用回调函数来处理逆向引用。 例如:
可以使用标准的 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';
这么牛B的功能是怎么实现的呢?先看一张Router工作的流程图:
所以核心入口是 __construct()
Router类大量代码处理的是自定义路由,该类要支撑以下几个功能点:
1、自定义路由规则
在 application/config/routes.php 文件中的 $route 的数组,利用它可以设置路由规则。 在路由规则中可以使用通配符或正则表达式。
使用通配符:$route['product/:num'] = 'catalog/product_lookup';
使用正则:$route['products/([a-z]+)/(\d+)'] = '$1/id_$2';
(:num) 匹配只含有数字的一段。 (:any) 匹配含有任意字符的一段。(除了 '/' 字符,因为它是段与段之间的分隔符)
通配符实际上是正则表达式的别名,:any 会被转换为 [^/]+ , :num 会被转换为 [0-9]+ 。
$key = str_replace(array(':any', ':num'), array('[^/]+', '[0-9]+'), $key);
2、支持回调函数
在路由规则中使用回调函数来处理逆向引用。 例如:
$route['products/([a-zA-Z]+)/edit/(\d+)'] = function ($product_type, $id)
{
return 'catalog/product_edit/' . strtolower($product_type) . '/' . $id;
};
3、支持使用 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';
这么牛B的功能是怎么实现的呢?先看一张Router工作的流程图:
CI在CodeIgniter.php中实例化路由时,就完成了解析,得到请求的控制器名及方法名了。
$RTR =& load_class('Router', 'core', isset($routing) ? $routing : NULL);所以核心入口是 __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);
// If a directory override is configured, it has to be set before any dynamic routing logic
//如果在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();
// Set any routing overrides that may exist in the main index file
//如果在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');
}
二、核心解析函数_set_routing()
protected function _set_routing()
{
// Load the routes.php file. It would be great if we could
// skip this for enable_query_strings = TRUE, but then
// default_controller would be empty ...
//加载路由配置文件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');
}
// Validate & get reserved routes
//读取默认控制器设置$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;
}
// Are query strings enabled in the config file? Normally CI doesn't utilize query strings
// since URI segments are more search-engine friendly, but they can optionally be used.
// If this feature is enabled, we will gather the directory/class/method a little differently
//在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)
{
// If the directory is set at this time, it means an override exists, so skip the checks
//获取$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();
}
// Routing rules don't apply to query strings and we don't need to detect
// directories, so we're done here
return;
}
// Is there anything to parse?
// 非querystring模式的程序可以走到这里
if ($this->uri->uri_string !== '')
{
//解析自定义路由规则,并调用_set_request函数设置目录、控制器、方法
$this->_parse_routes();
}
else
{
//uri_string为空,一般情况下就是域名后面没有任何字符,调用默认控制器
$this->_set_default_controller();
}
}
三、自定义路由解析函数
protected function _parse_routes()
{
// Turn the segment array into a URI string
//先将uri对像中的segments数组还原成uri路径。
$uri = implode('/', $this->uri->segments);
// Get HTTP verb
//获取http请求动作。
$http_verb = isset($_SERVER['REQUEST_METHOD']) ? strtolower($_SERVER['REQUEST_METHOD']) : 'cli';
// Loop through the route array looking for wildcards
//循环自定义路由规则,看是否能命中当前uri地址。
foreach ($this->routes as $key => $val)
{
// Check if route format is using HTTP verbs
//(功能3、支持使用 HTTP 动词) 处理HTTP 动词
//可以在你的路由规则中使用 HTTP 动词(请求方法),就是在路由数组后面再加一个键,键名为 HTTP 动词。
//标准的 HTTP 动词(GET、PUT、POST、DELETE、PATCH)
//比如定义了:$route['admin/pages']['get'] = 'admin/pages/view/about';
//那么当命中这条时:key:'admin/pages', val:array ('get' => 'admin/pages/view/about', )
if (is_array($val))
{
$val = array_change_key_case($val, CASE_LOWER);
if (isset($val[$http_verb]))
{
$val = $val[$http_verb];
}
else
{
continue;
}
}
// Convert wildcards to RegEx
//把:any和:num转成正则
$key = str_replace(array(':any', ':num'), array('[^/]+', '[0-9]+'), $key);
// Does the RegEx match?
匹配路径信息
if (preg_match('#^'.$key.'$#', $uri, $matches))
{
// Are we using callbacks to process back-references?
//(功能2、支持回调函数) 处理回调函数这种使用方法
if ( ! is_string($val) && is_callable($val))
{
// Remove the original string from the matches array.
//matches数组的第一个元素是能匹配上的完整字符串,所以先要把这个去掉,剩下的就是匹配上的括号中间表达式
array_shift($matches);
// Execute the callback using the values in matches as its parameters.
$val = call_user_func_array($val, $matches);
}
// Are we using the default routing method for back-references?
//(功能1、自定义路由规则) 处理通常的自定义路由
elseif (strpos($val, '$') !== FALSE && strpos($key, '(') !== FALSE)
{
//最核心的就是preg_replace这里了,不得不佩服正则函数的强大
$val = preg_replace('#^'.$key.'$#', $val, $uri);
}
//调用_set_request设置$this->directory、$this->class、$this->method
//参数是地址经过路由解析后再用'/'分割的数组
$this->_set_request(explode('/', $val));
return;
}
}
// If we got this far it means we didn't encounter a
// matching route so we'll set the site default route
//如果程序执行到这里,说明没有匹配任何路由规则
//调用_set_request设置$this->directory、$this->class、$this->method
$this->_set_request(array_values($this->uri->segments));
}
四、设置目录、控制器和方法名
protected function _set_request($segments = array())
{
//从$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))
{
$this->_set_default_controller();
return;
}
//如果允许路径中破折号存在,也就是路径中破折号'-'映到至类名的下划线 '_'
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';
}
//将整个数组元素往后推一格,保持和没有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;
}
五、从uri片段中抽取目录
protected function _validate_request($segments)
{
$c = count($segments);
$directory_override = isset($this->directory);
// Loop through our segments and return as soon as a controller
// is found or when such a directory doesn't exist
//支持多级目录
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')
&& $directory_override === FALSE
&& is_dir(APPPATH.'controllers/'.$this->directory.$segments[0])
)
{
$this->set_directory(array_shift($segments), TRUE);
continue;
}
return $segments;
}
// This means that all segments were actually directories
return $segments;
}