Hex 发表于 2013-4-10 17:35:53

CodeIgniter 源码分析之 Router.php

原文: http://blog.163.com/wu_guoqing/blog/static/19653701820128167043962/
作者: Calix

<?phpif ( ! defined('BASEPATH')) exit('No direct script access allowed');

// ------------------------------------------------------------------------

/**
* Router Class
*/
class CI_Router {

/**
* Config class
*/
var $config;

/**
* List of routes
*/
var $routes   = array();

/**
* List of error routes
*/
var $error_routes = array();

/**
* Current class name
*/
var $class   = '';

/**
* Current method name
*/
var $method   = 'index';

/**
* Sub-directory that contains the requested controller class
*/
var $directory= '';

/**
* Default controller (and method if specific)
*/
var $default_controller;

/**
* Constructor
*/
function __construct()
{
$this->config =& load_class('Config', 'core');
$this->uri =& load_class('URI', 'core');
log_message('debug', "Router Class Initialized");
}

// --------------------------------------------------------------------

/**
* Set the route mapping
*/
function _set_routing()
{

//如果项目是允许通过query_strings的形式,并且有通过$_GET的方式请求控制器的话,则以query_string形式,也就是
//?c=xx的形式确定路由。
$segments = array();
if ($this->config->item('enable_query_strings') === TRUE AND isset($_GET[$this->config->item('controller_trigger')]))
{
   //上面这里为什么还要判断有没有通过get的方式指定控制器?其实是因为如果允许query_string的形式请求路由,但是却
   //没有通过query_string(或者说是get)的形式指定路由的话(其实就说明这个通过query_string方式的uri是无效的),
   //此时,我们依然会采用“段”的形式。
   
   
   //取得目录名,目录名,控制名和方法名传递的变量名都是可以自定义的,在config/config.php里面。
   if (isset($_GET[$this->config->item('directory_trigger')]))
   {
    $this->set_directory(trim($this->uri->_filter_uri($_GET[$this->config->item('directory_trigger')])));
    $segments[] = $this->fetch_directory();
   }

   //取得控制器名
   if (isset($_GET[$this->config->item('controller_trigger')]))
   {
    $this->set_class(trim($this->uri->_filter_uri($_GET[$this->config->item('controller_trigger')])));
    $segments[] = $this->fetch_class();
   }

   //取得方法名
   if (isset($_GET[$this->config->item('function_trigger')]))
   {
    $this->set_method(trim($this->uri->_filter_uri($_GET[$this->config->item('function_trigger')])));
    $segments[] = $this->fetch_method();
   }
}
//。。。。。。。。。。。。。。。位置1

// Load the routes.php file.
//引入关于路由方面的配置信息。配置文件里面是一个名字为$route的数组。
if (defined('ENVIRONMENT') AND is_file(APPPATH.'config/'.ENVIRONMENT.'/routes.php'))
{
   include(APPPATH.'config/'.ENVIRONMENT.'/routes.php');
}
elseif (is_file(APPPATH.'config/routes.php'))
{
   include(APPPATH.'config/routes.php');
}

//下面这个莫名出现的$route变量(注意不是$this->routes哦),就是写在配置文件里面。把它copy到$this->routes。
//这个$routes是用来指定默认控制器和默认方法,404(请求路由不存在)后规定的路由以及一些路由重定向(?这个重写向的
//实现在Router::_parse_routes()中实现)的信息。
$this->routes = ( ! isset($route) OR ! is_array($route)) ? array() : $route;
unset($route);//利用完就干掉。

//根据刚才的配置信息,设定默认控制器,没有的话,就为FLASE。
$this->default_controller = ( ! isset($this->routes['default_controller']) OR $this->routes['default_controller'] == '') ? FALSE : strtolower($this->routes['default_controller']);


//这个判断的位置放得有点怪,我觉得可以放到上面“位置1”的地方,下面的代码是判断刚才有没有通过query_string的方式拿到
//路由信息,如果拿到的话,那么就不再尝试“段”的方式确定路由了。直接确定路由,结束本函数。
if (count($segments) > 0)
{
   //_validate_quest($segments);的作用就是确定并设置路由。
   //这个函数执行完之后,Router::$class,Router::$directory(如果有)都会有相应值。
   return $this->_validate_request($segments);
}

//下面的_fetch_uri_string()详见:URI.php,在这个位置只需知道它的作用是:
//从uri中检测处理,把我们确定路由需要的信息(例如“index.php/index/welcome/1”后
//面"index/welcome/1"这串)放到$this->uri->uri_string这个东西中。
$this->uri->_fetch_uri_string();

//上面_fetch_uri_string()完了之后,这个uri_string就会有我们要用的信息,如果为空的话,那么就用把路由设置为默认的。
if ($this->uri->uri_string == '')
{
   //移步至Router::_set_default_controller();
   return $this->_set_default_controller();
}


//如果$this->uri->uri_string 不为空,那么,会通过下面的方式确定路由

//这里只是简单地把后缀去掉而已,因为CI允许在uri后面加后缀,但它其实对我们寻找路由是多余,而且会造成影响的,所以先去掉。
$this->uri->_remove_url_suffix();

//把最初的uri,变成数组放在segments里面。
$this->uri->_explode_segments();

//开始找路由。移步至 Router::_parse_routes();
$this->_parse_routes();

//设置为由1开始。
$this->uri->_reindex_segments();
}

// --------------------------------------------------------------------

/**
* Set the default controller
*/
function _set_default_controller()
{
//在Router::_set_routing()函数里面有一个操作,是从配置文件里面读取默认控制器名,如果没有就有FALSE。
if ($this->default_controller === FALSE)
{
   //如果没有默认的话,就报错,结束程序。
   //实质上,这个_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 (strpos($this->default_controller, '/') !== FALSE)
{
   $x = explode('/', $this->default_controller);

   $this->set_class($x);
   $this->set_method($x);
   $this->_set_request($x);//移步至Router::_set_request();
}
else
{
   $this->set_class($this->default_controller);
   $this->set_method('index');
   $this->_set_request(array($this->default_controller, 'index'));
}

// re-index the routed segments array so it starts with 1 rather than 0
$this->uri->_reindex_segments();

log_message('debug', "No URI present. Default controller set.");
}

// --------------------------------------------------------------------

/**
* Set the Route
*/
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 = $this->_validate_request($segments);

if (count($segments) == 0)
{
   //所以如果上面返回了空数组,就会进到这里。
   //这里居然又调回了_set_default_controller()! 坑爹吧!
   return $this->_set_default_controller();
   /**
    * 我曾经想过,下面这里会不会死循环:
    * 假如,我在配置文件里面的默认控制器设为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’还有一个作用,就是设置默认方法啦。
    */   
}

$this->set_class($segments);

if (isset($segments))
{
   // A standard method request
   $this->set_method($segments);
}
else
{
   $segments = 'index';
}


//这里要说一下,现在是在ROUTER里面为URI赋值,URI里面的这个URI::$rsegments是经过处理,并确定路由后,实质调用的路由的段信息。
//而URI::$segments (前面少了个r),则是原来没处理前的那个,即直接由网址上面得出来的那个。
$this->uri->rsegments = $segments;
}

// --------------------------------------------------------------------

/**
* Validates the supplied segments.Attempts to determine the path to
* the controller.
*/
function _validate_request($segments)
{
if (count($segments) == 0)
{
   return $segments;
}

if (file_exists(APPPATH.'controllers/'.$segments.'.php'))
{
   //如果直接在controllers这个目录下找到与第一段相应的控制器名,那就说明找到了控制器,确定路由,返回。
   return $segments;
}

//如果上面没有找到,再看看这个“第一段”是不是一个目录,因为CI是允许控制器放在自定义的目录下的。
if (is_dir(APPPATH.'controllers/'.$segments))
{
   // Set the directory and remove it from the segment array
   //如果的确是目录,那么就可以确定路由的目录部分了。
   $this->set_directory($segments);
   //去掉目录部分。进一步进行路由寻找。
   $segments = array_slice($segments, 1);

   //如果uri请求中除了目录还有其它“段”,那说明是有请求某指定控制器的。
   if (count($segments) > 0)
   {
    //指定请求的控制器找不到的话,那只好报错了。
    if ( ! file_exists(APPPATH.'controllers/'.$this->fetch_directory().$segments.'.php'))
    {
   //报错也有两方式,一种是默认的,一种是自义定的。
   //下面这个404_override就是在config/routes.php定义的一个路由找不到时候的默认处理控制器了,如果有定义
   //我们调用它。
   if ( ! empty($this->routes['404_override']))
   {
      $x = explode('/', $this->routes['404_override']);

      $this->set_directory('');//把刚才设置好的路由的目录部分去掉,因为现在路由是我们定义的404路由。
      $this->set_class($x);//这里可以看出,我们定义的404路由是不允许放在某个目录下的,只能直接放在controllers/下
      $this->set_method(isset($x) ? $x : 'index');//默认是index方法

      return $x; //同样,返回“段”数组
   }
   else
   {
      //默认找不到路由的方法。在core/Common.php中定义的全局函数(实质调用Exception组件进行处理)。
      show_404($this->fetch_directory().$segments);
   }
    }
   }
   else
   {
    //来到这里,说明了是uri请求指定了目录,而没有指定控制器的情况下。那么,我们默认当前路由是在当前目录下请求默认的
    //控制器和方法。
   
    //下面这个判断只是判断一下$this->default_controller有没有指定方法而已。
    if (strpos($this->default_controller, '/') !== FALSE)
    {
   $x = explode('/', $this->default_controller);
   $this->set_class($x);
   $this->set_method($x);
    }
    else
    {
   $this->set_class($this->default_controller);
   $this->set_method('index');//没有的话就默认为index方法。
    }

    // Does the default controller exist in the sub-folder?
    //如果连默认控制器都不存在的话,就无语了,说明uri打算请求这个目录的默认控制器,结果没有这个默认控制器,那暂时
    //返回个空数组。(但是看清楚,上面已经$this->set_class()了,说明即使没有,我们也已经把默认控制器的名字算
    //是确定下来先)
    if ( ! file_exists(APPPATH.'controllers/'.$this->fetch_directory().$this->default_controller.'.php'))
    {
   $this->directory = '';
   return array();
    }

   }
   
   //能够有命来到这一步,是说明的确是某个目录下找到了控制器,或者是找到了定义的默认控制器。
   //但是注意,这个$segments返回的“段”信息都是不包括目录的。它是一个数组形式,第一个元素是控制器名。
   //例如:array('acontroller','amethod','xx','xx')。。
   return $segments;
}

//来到这里,就说明了,即找不到controllers/下相应的控制器,也找不到这样的目录。那就报错咯。
if ( ! empty($this->routes['404_override']))
{
   $x = explode('/', $this->routes['404_override']);

   $this->set_class($x);
   $this->set_method(isset($x) ? $x : 'index');

   return $x;
}


// Nothing else to do at this point but show a 404
show_404($segments);
}

// --------------------------------------------------------------------

/**
*Parse Routes
*/
function _parse_routes()
{
// Turn the segment array into a URI string
//知道_set_request()是干嘛的之后,下面的条理就比较清晰了。
$uri = implode('/', $this->uri->segments);

// Is there a literal match?If so we're done
if (isset($this->routes[$uri]))
{
   return $this->_set_request(explode('/', $this->routes[$uri]));
}

/**
   * CI有路由重定向的功能,重定向的规则和实现就是在这里。
   */
foreach ($this->routes as $key => $val)
{
   $key = str_replace(':any', '.+', str_replace(':num', '+', $key));


   if (preg_match('#^'.$key.'$#', $uri))
   {
    // Do we have a back-reference?
    if (strpos($val, '$') !== FALSE AND strpos($key, '(') !== FALSE)
    {
   $val = preg_replace('#^'.$key.'$#', $val, $uri);
    }

    return $this->_set_request(explode('/', $val));
   }
}
$this->_set_request($this->uri->segments);
}

// --------------------------------------------------------------------

/**
* Set the class name
*/
function set_class($class)
{
$this->class = str_replace(array('/', '.'), '', $class);
}

// --------------------------------------------------------------------

/**
* Fetch the current class
*/
function fetch_class()
{
return $this->class;
}

// --------------------------------------------------------------------

/**
*Set the method name
*/
function set_method($method)
{
$this->method = $method;
}

// --------------------------------------------------------------------

/**
*Fetch the current method
*/
function fetch_method()
{
if ($this->method == $this->fetch_class())
{
   return 'index';
}

return $this->method;
}

// --------------------------------------------------------------------

/**
*Set the directory name
*/
function set_directory($dir)
{
$this->directory = str_replace(array('/', '.'), '', $dir).'/';
}

// --------------------------------------------------------------------

/**
*Fetch the sub-directory (if any) that contains the requested controller class
*/
function fetch_directory()
{
return $this->directory;
}

// --------------------------------------------------------------------

/**
*Set the controller overrides
*/
function _set_overrides($routing)
{
if ( ! is_array($routing))
{
   return;
}

if (isset($routing['directory']))
{
   $this->set_directory($routing['directory']);
}

if (isset($routing['controller']) AND $routing['controller'] != '')
{
   $this->set_class($routing['controller']);
}

if (isset($routing['function']))
{
   $routing['function'] = ($routing['function'] == '') ? 'index' : $routing['function'];
   $this->set_method($routing['function']);
}
}


}

cifj 发表于 2014-4-17 17:32:04

总算看懂了,大概吧...
感觉应该从三条线整理LZ的思路
1.index.php
2.index.php/c/a
3.在config/routes.php中设置了带目录的默认控制器$route['default_controller'] = "hello/home";
或者直接index.php/hello/home,反正是hello.php在/目录下没找着,找到了hello目录

然后贯穿整个线路
先是_set_routing()的各种判断,然后出现分支
a.如果是1情况,就从_set_default_controller()前往_set_request()
b.如果是2情况,就从_parse_routes()前往_set_request()

无论a还是b的_set_request()都会经过_validate_request()的过滤,因为会有3情况的出现,3比较特殊,他是隐藏在1和2之中的,当然3也会过滤到_set_default_controller()里的另外一个判断,最后又回到_validate_request()

最终进行set_class()和set_method()

不知道理解的对不对,明天继续学习uri.php
页: [1]
查看完整版本: CodeIgniter 源码分析之 Router.php