CI框架能根据URL地址路由到正确的控制器和方法,主要是靠URI和Router这两个组件,URI组件对URL地址所携带的参数进行分段,Router组件再根据URI解析出来的分段参数找到对应的控制器,方法,及调用方法所需的参数,并且Router还能通过正则表达式自定义路由方式。 在阅读路由组件的源码时,我们需要思考以下几个问题: 1.Router是怎样根据URI的分段参数找到对应的类和方法? 2.自定义路由是怎样实现的? 带着以上几个问题我们开始阅读Router的源码:
1.构造函数
/**
* Class constructor
*
* Runs the route mapping function.
*
* @param array $routing
* @return void
*/
public function __construct($routing = NULL)
{
$this->config =& load_class('Config', 'core');
$this->uri =& load_class('URI', 'core');
$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
is_array($routing) && isset($routing['directory']) && $this->set_directory($routing['directory']);
$this->_set_routing();
// Set any routing overrides that may exist in the main index file
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()函数才是真正实现路由的地方,那么接下来我们具体看看这个函数。
2._set_routing()
_set_routing()这个函数是主要实现路由的地方,它可以根据uri请求确定路由的内容,并且实现在路由配置文件中设置的路由方式 这个函数的代码可以分为三部分来阅读: 1.加载routes配置文件 2.query_string路由方式 3.非query_string路由方式
/**
* Set route mapping
*
* Determines what should be served based on the URI request,
* as well as any "routes" that have been set in the routing config file.
*
* @return void
*/
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 ...
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');
}
// Validate & get reserved 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
if ($this->enable_query_strings)
{
// If the directory is set at this time, it means an override exists, so skip the checks
if ( ! isset($this->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]))
{
$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?
if ($this->uri->uri_string !== '')
{
$this->_parse_routes();
}
else
{
$this->_set_default_controller();
}
}
在这个函数中我们可以看到通过query_string参数路由的实现有以下几个步骤: 1.取出config配置中的directory_trigger,controller_trigger,function_trigger三个参数 2.取出query_string中参数名为以上三个配置项参数的值 3.将对应的query_string参数存储到对应的变量中 4.将路由参数以数组形式赋值给uri->rsegments变量中,路由完成
接下来我们再看一下_parse_toutes()函数,看其他情况是如何实现路由的
3._parse_routes()
这个函数实现的路由方式就是我们平常常见的ci路由地址,例如:http://dump.com/test/t_fun,会路由到test控制器的t_fun方法。 先看源码:
/**
* Parse Routes
*
* Matches any routes that may exist in the config/routes.php file
* against the URI to determine if the class/method need to be remapped.
*
* @return void
*/
protected function _parse_routes()
{
// Turn the segment array into a URI string
$uri = implode('/', $this->uri->segments);
// Get HTTP verb
$http_verb = isset($_SERVER['REQUEST_METHOD']) ? strtolower($_SERVER['REQUEST_METHOD']) : 'cli';
// Loop through the route array looking for wildcards
foreach ($this->routes as $key => $val)
{
// Check if route format is using HTTP verbs
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
$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?
if ( ! is_string($val) && is_callable($val))
{
// Remove the original string from the matches array.
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?
elseif (strpos($val, '$') !== FALSE && strpos($key, '(') !== FALSE)
{
$val = preg_replace('#^'.$key.'$#', $val, $uri);
}
$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
$this->_set_request(array_values($this->uri->segments));
}
到这里我们可以回答第二个问题了,Router用routes配置的key匹配uri,用key对应的值指定处理方式,可以调用函数处理,也可以直接用正则替换,以此实现了自定义路由格式。 自定义路由格式最终还是要被转换成和URI中分段参数一样的格式,再由set_request函数处理 接下来我们看一下set_request函数的源码
4._set_request
/**
* Set request route
*
* Takes an array of URI segments as input and sets the class/method
* to be called.
*
* @used-by CI_Router::_parse_routes()
* @param array $segments URI segments
* @return void
*/
protected function _set_request($segments = array())
{
$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!
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
{
$segments[1] = 'index';
}
array_unshift($segments, NULL);
unset($segments[0]);
$this->uri->rsegments = $segments;
}
到这里整个路由都实现了,segments第一个参数为控制器,第二个参数为方法 这里有几需要注意的地方: rsegments的数组下标也是从1开始的,和URI的segments一样。