用户
 找回密码
 入住 CI 中国社区
搜索
查看: 43704|回复: 42
收起左侧

[中级] CodeIgniter 源代码解析

    [复制链接]
发表于 2009-8-21 14:21:10 | 显示全部楼层 |阅读模式
作者:blacktear23


        废话

        接触CodeIgniter(以下简称CI)的时间并不长,不过出于好奇心,还是花了一点时间来研究了一下CI到底在私下做了些什么。不想独自受用,拿出来与大家分享,不过过程之间还有一些问题和不正之处,还望大家能有所交流。
        好了,废话过了,切入主题吧。

        从Hello World开始

        用CI写一个Hello World并不复杂,但这里我想用一个稍微复杂一点点的例子来开始:“Hello World”这个字符串从Controller通过参数传入。当然,比起重头写一个Controller,改改CI中自带的Welcome控制器可能更加简便一些:
        
controllers\Welcome.php
PHP复制代码
<?php
class Welcome extends Controller {
 
        function Welcome()
        {
                parent::Controller();
        }
       
        function index()
        {
                $data = array('msg' => 'Hello World!');
                $this->load->view('welcome_message',$data);
        }
}
?>
复制代码

        
views\welcome_message.php
PHP复制代码
<html>
<head>
<title><?php echo $msg;?></title>
</head>
<body>
 
<h1><?php echo $msg;?></h1>
</body>
</html>
复制代码

        
        当然,上面的代码运行起来的样子很容易就想象出来。不过,当我们在浏览器中输入URL之后,到浏览器获得返回页面这段时间,CI到底做了些什么呢?

        一切由index.php而起

        一个CI的Web程序的入口是根目录中的index.php。下面来看看index.php中到底都做了一些什么事情。其实这个文件中的代码相当简单,除了对一些路径的定义之外就是require CodeIgniter.php这个文件。
        在index.php中,CI首先做的事情就是设置PHP的错误报告,上来都是E_ALL,如果不想让所有问题都显示,改改这个地方没啥难度。
        接下来定义了两个变量,用来指定system文件夹的名字和application文件夹的名字。接下来CI用system文件夹变量的内容转为完全的路径(比如:C:\wwwroot\ci\system)。到了后面就是定义一些常量,这些常量的含义从名字就能猜出一二来了。这些常量定义完成之后,index.php把接下来的工作完全交给了CodeIgniter.php这个文件了。
        唠叨两句:对于CI的BASEPATH和APPPATH两个常量来说,CI的策略是APPPATH默认是在BASEPATH下面。不过想起了Zend Framework可以把框架和应用进行分离,因此觉得CI在这里的设计还是有点死板。不过这也可能是CI本身的一个特点吧。

        CodeIgniter.php

        这个文件可以说是CI的核心,大部分MVC的流程都是在这个文件中处理的。当然,这需要一些CI核心类的配合。顺序往下看,不难看出这个文件中的代码思路还是比较清晰的。
        首先是定义了CI的版本(我这里是CI 1.7.0)。接下来是连续require了3个文件,Common.php、Compat.php、constants.php。前两个是CI的文件,后一个是应用程序中config文件夹中的文件。
        从文件名和注释来看,Common.php是一些全局函数,Compat.php里面是一些兼容用的函数。其实打开Compat这个文件,里面只有2个函数和一个常量,函数的用途很容易就能看明白,这里就不多说了。
        而对于Common.php这个文件来说,就稍微复杂了一些。虽然只有8个函数,但这8个函数使用之频繁,在看了后面的代码之后,自然就感觉出来了。
        接下来继续往下看,是设置错误处理,其中的“_exception_handler”这个函数可以在Common.php文件中找到。
        有了一些基础的函数之后,就该切入正题了:
        
CodeIgniter.php中的代码片段
PHP复制代码
/*
 * ------------------------------------------------------
 *  Start the timer... tick tock tick tock...
 * ------------------------------------------------------
 */

 
$BM =& load_class('Benchmark');
$BM->mark('total_execution_time_start');
$BM->mark('loading_time_base_classes_start');
复制代码

        
        难以想象的是CI第一个加载的类居然是Benchmark!对于load_class这个函数来说,暂时先不要管它到底怎么做的,只要知道他是用来加载CI的核心类并创建对应的实例即可。Benchmark类的mark方法非常简单:
        
Benchmark类的mark方法
PHP复制代码
/**
 * Set a benchmark marker
 *
 * Multiple calls to this function can be made so that several
 * execution points can be timed
 *
 * @access        public
 * @param        string        $name        name of the marker
 * @return        void
 */

function mark($name)
{
        $this->marker[$name] = microtime();
}
复制代码

        
        这里只是用时间打一个戳罢了。
        唠叨两句:其实从这里来看,CI的Benchmark一般对性能不会产生多大影响。因为mark方法并没有做什么复杂的运算。如果觉得这个简单的mark方法还是比较浪费,那么干脆就自己扩展一个Benchmark,然后直接return算了。
        CI第二个加载的类是Hooks。说是hook,其实把它理解成Event Listener也算可以吧。加载了Hooks之后,CI做的第一件事情就是调用注册为“pre_system”的hook(也可以理解成CI在这里出发了一个pre_system事件,所有注册了这个事件的监听器都会被调用)。
        接下来就是顺序地加载了Config、URI、Router、Output四个类。之后的cache_override的hook如果没有注册的话,CI就会调用Output的_display_cache方法来直接输出缓存。如果缓存命中的话,方法会返回true,之后脚本就会停止运行。否则就继续运行。
        接下来还是加载类,Input和Language。不过后面就有点意思了,CI会根据PHP的版本require不同的文件:Base4.php或Base5.php。4和5的差别就是4的CI_Base类是继承了CI_Loader类,而5则没有。而且我们之后会在helper等其他地方用到的get_instance函数也是这里定义的。其实这个函数就是返回了CI_Base这个类的一个实例。不过由于Controller本身就继承自这个类,所以get_instance函数可以得到当前的Controller对象。
       唠叨两句:在Base5.php中,我们应该注意到这么一点,即这个文件是require进来的,并没有做任何的实例化操作。而相应的阅读一下Controller的构造函数,我们会发现,其实只有在对应的Welcome这个Controller类的子类实例化之后,才会逐级调用构造函数,一直到CI_Base里。因此这时CI_Base中的$this其实就是Welcome类的实例。
        在CI_Base加载之后,就是加载Controller类。这里的load_class函数多了一个参数,并设置成false,这个意思是只加载Controller这个类对应的文件,而不构造对应的实例。
        接下来就是重点了,加载对应的Controller类。在这里应该是Welcome类。
        
CodeIgniter.php的代码片段
PHP复制代码
// Load the local application controller
// Note: The Router class automatically validates the controller path.  If this include fails it
// means that the default controller in the Routes.php file is not resolving to something valid.
if ( ! file_exists(APPPATH.'controllers/'.$RTR->fetch_directory().$RTR->fetch_class(). EXT))
{
        show_error('Unable to load your default controller.  Please make sure the controller specified in your Routes.php file is valid.');
}
 
include(APPPATH.'controllers/'.$RTR->fetch_directory().$RTR->fetch_class().EXT);
复制代码

        
        上面这段代码就是用来装载对应的Controller类。$RTR就是Router类的实例。Router类在构造实例的时候就已经对请求的URL进行了解析,并得出对应的类名和方法名,而上面这段代码就是通过fetch_class方法来获得对应的类名。如果这个控制器的文件存在,就加载这个文件,否则就报错。
        下面就是判断类是否存在和对应的方法是否在类中:
        
CodeIgniter.php的代码片段
PHP复制代码
/*
 * ------------------------------------------------------
 *  Security check
 * ------------------------------------------------------
 *
 *  None of the functions in the app controller or the
 *  loader class can be called via the URI, nor can
 *  controller functions that begin with an underscore
 */

$class  = $RTR->fetch_class();
$method = $RTR->fetch_method();
 
if ( ! class_exists($class)
        OR $method == 'controller'
        OR strncmp($method, '_', 1) == 0
        OR in_array(strtolower($method), array_map('strtolower', get_class_methods('Controller'
)))
        )
{
        show_404("{$class}/{$method}");
}
复制代码

        
        代码不难理解,首先是看是否有这个类,然后就是看看要调用的函数是否是以“_”开头(也就是CI中约定的私有方法),或者是在Controller这个类中的方法。如果这些都检测通过,就继续往下走,否则就返回404响应。当然,这个404响应的页面也是CI自定义的。(show_404这个函数可以从Common.php中找到)
        下面就是构造一个Welcome这个类的实例了:
      
PHP复制代码
$CI = new $class();
复制代码

        后面有个判断,通过Router对象判断这个请求是否是脚手架(scaffolding)返回的。这里先不用管脚手架的问题,暂且都认为是非脚手架的请求,那么就到了else下面的代码段了。
        在else后面的代码段中,可以意外发现一个叫_remap的方法。不过上面的代码中并没有_remap这个方法,而且Controller、CI_Base两个类中也没有相应的方法。想必这是作者留下来的一个扩展点(不过好像没有看到文档中对此有所介绍)。
        自然,上面的代码是没有_remap这个方法的。所以就到了else后面。这里判断了一下对应的方法是否在Welcome类中,如果在就调用之,否则还是404。
        之后的另一个关键点就是:
        
CodeIgniter.php代码片段
PHP复制代码
if ($EXT->_call_hook('display_override') === FALSE)
{
        $OUT->_display();
}
复制代码

        
        这里与上面的缓存处理的思路类似。如果没有注册display_override的hook,CI就调用Output类实例的_display方法来显示对请求的响应。
        最后是调用post_system的hook,并关闭数据库。
        唠叨两句:关闭数据库的代码默认的是Controller对象中要有db这个属性。看来是写死了。
        到此,整个请求已经处理完毕了。不过还有一些其他的相关的内容还没有深入地了解:
  • load_class是怎么工作的
  • Config类到底做了些什么
  • Loader类到底是怎么回事
  • Router怎么解析请求的
  • Controller类到底做了些什么

        这些将在后面逐一解析。

        load_class到底是怎么干活的

        其实load_class做了两件事:
  • 装在对应类的php文件
  • 根据$instance参数决定是否创建对应类的对象

        load_class函数还自带了一个缓存——$object静态变量。
        对于任何一次函数的调用,load_class函数都会先从缓存中寻找对应的值,这样可以节省一些重复运行一些代码的时间,也算是应用了单件这个设计模式。
        
Common.php文件中load_class函数的代码片段
PHP复制代码
// If the requested class does not exist in the application/libraries
// folder we'll load the native class from the system/libraries folder.        
if (file_exists(APPPATH.'libraries/'.config_item('subclass_prefix').$class.EXT))
{
        require(BASEPATH.'libraries/'.$class.EXT);
        require(APPPATH.'libraries/'.config_item('subclass_prefix').$class.EXT);
        $is_subclass = TRUE;
}
复制代码

        
        这段代码可以让我们了解CI是怎么让用户扩展自己的核心类库的。首先load_class会检查在APPPATH中的libraries文件夹中是否有以subclass_prefix开头的文件。比如我们的subclass_prefix值为“MY_”,那么当调用load_class(“Controller”)时,load_class会在APPPATH中的libraries文件夹中找“MY_Controller.php”这个文件。如果有这个文件的话,load_class会先require系统文件夹中libraries文件夹中的Controller.php文件,然后再加载“MY_Controller.php”文件。毕竟MY_Controller.php文件中的类是需要继承Controller.php文件中的类的。
        当然,如果没有用户扩展基础类库的类的话,load_class函数会先从APPPATH的libraries中找对应的文件,之后才是BASEPATH的libraries中的系统自带的基础类库。换句话说就是如果我们在APPPATH的libraries中定义了一个跟基础类相同的文件,那么我们就覆盖(可能说屏蔽会好点)了这个基础类。
        后面的代码就是来判断是否要创建类对应的实例了。如果不需要创建,那么就把缓存设置成true。否则就是创建实例,然后把缓存设置成对应的实例。在创建实例是,类名是一个需要转换一下的内容。对于非扩展类来说,除了Controller类之外,其他的类都会加入CI_这个前缀。而对于扩展类来说,所有的类名都会加入用户定义的前缀,例如“MY_”。

        Config类到底是怎么工作的

        打开Config.php这个文件,会发现CI_Config的构造函数中只是调用了get_config函数,并把返回值赋给了$config属性。get_config函数是在Common.php文件中,主要用来从APPPATH\config文件夹中读取config.php文件中的配置属性。当然,第一行的
      
PHP复制代码
static $main_conf
复制代码

说明了这个函数也有个“缓存”,以免重复从config.php文件中读取配置信息。
        在CI_Config类中还有几个其他的方法,代码并不复杂,而且用途从手册中都能了解到,也就不在废话了。
        唠叨几句:对于CI_Config类,觉得这个类有点太傀儡了。其实看看整个CI框架中对配置信息的引用,多是使用get_config函数或者是config_item函数。而CI_Config类也仅仅是为了OO而重新封装了一下而已。虽然提供了几个辅助性的函数,但也并不是缺一不可。而且对于配置信息来说,CI的处理并不一视同仁,后面读到Router类的时候就会有所体现了。这不得不说是CI在OO方面的一个“败笔”吧。

评分

参与人数 1威望 +5 收起 理由
a306211321 + 5 很给力!

查看全部评分

发表于 2014-8-3 01:08:27 | 显示全部楼层
写的不错,如果在配上图就更好了
发表于 2014-11-6 11:44:40 | 显示全部楼层
做一下记录,到中级时才看
发表于 2016-11-14 18:14:01 | 显示全部楼层
真的挺好的!
 楼主| 发表于 2009-8-22 01:32:52 | 显示全部楼层
(接楼上)

Loader类是怎么工作的

        对于CI来说,在实际编程的时候与Loader打交道可是非常多的。针对不同的资源调用Loader的不同方法。而这也是CI的一个核心,或者也可以说是CI的一个亮点。(个人觉得CI的作者在类的加载方面还是深有体会的。)
        首先来看看CI_Loader的构造函数,很简单,只是设置3个变量:
        _ci_is_php5:是不是PHP 5的版本;
        _ci_view_path:视图文件的路径;
        _ci_ob_level:PHP输出缓冲的级别。
        对于最后一行的log_message就先不管它了。
        对于Loader的方法,一般来说,library、model、database、view、config、helper这几个方法用的比较多。首先来看看library方法。

        library方法

        首先是过滤一些非法参数,接下来如果$library是数组的话,就逐个调用_ci_load_class方法。反之就真对一个库调用_ci_load_class方法。最后是调用_ci_assign_to_models方法,这个方法的目的是把刚加载的类的实例注入到所有的Model中去。当然,要了解CI如何做到这一点的,就需要从Model.php中找答案了。这里简单介绍一下:
        CI_Model类中的_assign_libraries方法会从get_instance函数获得的Controller实例获取所有的属性,然后逐个赋值到Model类当中。
        接下来,要介绍的重头戏就是_ci_load_class方法了。这个方法的做法跟get_class函数差不多。
        在CI中,可以用“path/class”的方式来装载类,下面的代码就是用来实现这个功能的一部分:分开path和class。
        
_ci_load_class方法的代码片段
PHP复制代码
$subdir = '';
if (strpos($class, '/') !== FALSE)
{
        // explode the path so we can separate the filename from the path
        $x = explode('/', $class);        
       
        // Reset the $class variable now that we know the actual filename
        $class = end($x);
       
        // Kill the filename from the array
        unset($x[count($x)-1]);
       
        // Glue the path back together, sans filename
        $subdir = implode($x, '/').'/';
}
复制代码

        
        首先是检查是否有“/”这个字符,然后是用“/”展开$class为数组,然后$class被设置为数组的最后一个元素,最后是删除了数组最后一个元素并把这个数组用“/”连接起来,赋值给$subdir。
        接下来的foreach看起来比价有意思,用于foreach的数组中包含了2个元素:$class字符串的首字母大写形式和$class的全小写模式。
        好了,看看foreach内部的代码:
        首先是检查是否有扩展的子类,这个原理跟load_class函数一样。然后就是判断需要加载的类是否已经加载,如果已经加载,就再看看当前的Controller对象中是否已经有了与$object_name的值同名的属性,如果没有就设置这个属性,并把值赋为要加载类的实例。否则就什么都不做。如果没有装载过这个类,那么该干什么相比都能猜出来了。
        以上是对扩展类的一些解析,而对于非扩展类,代码显得更有意思:
        
_ci_load_class方法的代码片段
PHP复制代码
// Lets search for the requested library file and load it.
$is_duplicate = FALSE;                
for ($i = 1; $i < 3; $i++)
{
        $path = ($i % 2) ? APPPATH : BASEPATH;        
        $filepath = $path.'libraries/'.$subdir.$class.EXT;
       
        // Does the file exist?  No?  Bummer...
        if ( ! file_exists($filepath))
        {
                continue;
        }
       
        // Safety:  Was the class already loaded by a previous call?
        if (in_array($filepath, $this->_ci_loaded_files))
        {
                // Before we deem this to be a duplicate request, let's see
                // if a custom object name is being supplied.  If so, we'll
                // return a new instance of the object
                if ( ! is_null($object_name))
                {
                        $CI =& get_instance();
                        if ( ! isset($CI->$object_name))
                        {
                                return $this->_ci_init_class($class, '', $params, $object_name);
                        }
                }
       
                $is_duplicate = TRUE;
                log_message('debug', $class." class already loaded. Second attempt ignored.");
                return;
        }
       
        include_once($filepath);
        $this->_ci_loaded_files[] = $filepath;
        return $this->_ci_init_class($class, '', $params, $object_name);
}
复制代码


        乍一看,上面的for循环很匪夷所思——这是要干什么啊!但是往下看一行之后,就明白了。作者不过是想在APPPATH和BASEPATH之间进行一个切换(这与上面的foreach有异曲同工之妙)。$filepath自不用说了,if语句就是判断一下文件是否存在,不存在继续循环。
        接下来的if语句跟上面针对扩展类的操作方法一样,还是判断类是否已经加载,如果是再判断一下Controller实例中是否有了$object_name的值相同的属性,如果有则什么都不做,没有就创建这么个属性,并赋值为要加载类的实例。如果要加载的类还没有加载过,就include_once它,并设置这个类已经加载过,然后就是创建类的实例。
        剩下的代码就是做一些补救措施,以及出错提示,这里就不再讲了,代码不难。
        在这个方法中,_ci_init_class方法起了大作用,他是用来创建类的实例的。下面就来看看_ci_init_class到底做了什么。
        首先,_ci_init_class先从config文件夹中找与类名相对应的配置文件,然后加载,当然前提是$config这个参数的值是NULL:
_ci_init_class方法代码片段
PHP复制代码
// Is there an associated config file for this class?
if ($config === NULL)
{
        // We test for both uppercase and lowercase, for servers that
        // are case-sensitive with regard to file names
        if (file_exists(APPPATH.'config/'.strtolower($class).EXT))
        {
                include_once(APPPATH.'config/'.strtolower($class).EXT);
        }                        
        else
        {
                if (file_exists(APPPATH.'config/'.ucfirst(strtolower($class)).EXT))
                {
                        include_once(APPPATH.'config/'.ucfirst(strtolower($class)).EXT);
                }                        
        }
}
复制代码

        
        接下来就是搞定实际的类名。无非是处理类似“CI_Session”还是 “MY_Session”的问题。后面是处理$object_name的问题。既如果$object_name是NULL,则先看看_ci_varmap这个属性中是否有对应类名称的值,如果有则用这个值,否则就是类名。不过这时的类名称已经被转化为小写字母了。(这也就是当调用library(“Session”)时,默认的属性名是session的原因)
        剩下的工作就是构造这个类的对象,并把对象赋值给Controller实例的对应属性上。这里多出了一个$config变量,而这个变量是在最初include_once与类名对应的配置文件时设置的。
        唠叨两句:最后的创建类的实例时对构造函数添加参数的功能最明显的例子就是form_validation类了。在CI中,可以在config文件夹中建立一个form_validation.php文件,然后设置一些规则,系统会自动加载这个配置文件的规则。而这个魔法就发生在_ci_init_class方法中。
        另外还有两句要说的就是CI的Loader的设计并不怎么优秀,毕竟有一些功能重复地代码同时出现在Loader和load_class函数中。这并不是什么好的代码味道。而在_ci_load_class方法中的一些“歪门邪道”的小技巧相比对代码本身的美感有很大的负面影响。

        model方法

        当装载一个模型时就会用到它。整体思路其实与library相同,还是那几大步骤:
  • 分开path和class,如果有“/”的话;
  • 处理注册到Controller实例的属性名称;
  • 检查Model是否已经创建;
  • 创建对应的Model对象,并注册到Controller实例和_ci_models属性中。

        不过在这几个主要步骤中,Model还是有一些独特的地方。在创建Model对象之前会根据$db_conn的值来决定是否连接数据库。在对象创建之后调用_assign_libraries方法。

        database方法

        这个方法很简单,思路与上面的也差不多,也就不多说了,看看代码就明白了。对于database方法来说,DB.php这个文件则更有意思。不过这里并不想讨论这方面的内容。

        view方法

        这个可以说是一个经常打交道的方法,用来加载视图。view方法实际是调用了_ci_load方法来加载视图。在_ci_load方法中,首先映入眼帘的就是一段有意思的代码:
_ci_load代码片段
PHP复制代码
// Set the default data variables
foreach (array('_ci_view', '_ci_vars', '_ci_path', '_ci_return') as $_ci_val)
{
        $$_ci_val = ( ! isset($_ci_data[$_ci_val])) ? FALSE : $_ci_data[$_ci_val];
}
复制代码


        这区区4行代码的目的是为了在方法中声明4个变量:$_ci_view、$_ci_vars、$_ci_path、$_ci_return。不过这里CI的作者使用的招数也太诡异了。接下来的代码是确定要加载的文件的具体路径。
        接下来就是把Controller实例的属性值以引用的方式复制到Loader对象中。这样,在图中用$this可以访问到Controller中的属性。然后就是设置视图的参数,并用extract函数把数组中的值编程变量(这就是为什么在Controller中设置的数组的key可以在视图中用变量的方式访问)。
        接下来就切入正题了,开始加载视图。
_ci_load代码片段
PHP复制代码
ob_start();
               
// If the PHP installation does not support short tags we'll
// do a little string replacement, changing the short tags
// to standard PHP echo statements.
 
if ((bool) @ini_get('short_open_tag') === FALSE AND config_item('rewrite_short_tags') == TRUE)
{
        echo eval('?>'.preg_replace("/;*\s*\?>/", "; ?>", str_replace('<?=', '<?php echo ', file_get_contents($_ci_path))));
}
else
{
        include($_ci_path); // include() vs include_once() allows for multiple views with the same name
}
 
log_message('
debug', 'File loaded: '.$_ci_path);
 
// Return the file data if requested
if ($_ci_return === TRUE)
{                
        $buffer = ob_get_contents();
        @ob_end_clean();
        return $buffer;
}
 
if (ob_get_level() > $this->_ci_ob_level + 1)
{
        ob_end_flush();
}
else
{
        // PHP 4 requires that we use a global
        global $OUT;
        $OUT->append_output(ob_get_contents());
        @ob_end_clean();
}
复制代码


        首先是ob_start,用来开启输出缓冲。然后是判断是否替换短标记。替换短标记这块看上去有点复杂,其实就是先读出所有文件的内容,然后把“<?=”替换成“<?php echo”,然后在把末尾的“?>”替换成“; ?>”,最后用echo输出用eval函数执行了这些内容的结果。
        如果不需要进行替换的话,一个简单的includes就搞定了一切。如果之前的参数中设置了_ci_return参数,那么接下来的代码就会把缓冲区的内容取出来,作为结果返回。如果不需要返回值的话,就是把缓冲的结果传到Output对象中去。当然,如果缓冲区的级别比Loader中设置的级别高2级的话,系统会直接输出结果而不通过Output对象来输出响应结果。

        config和helper方法

        对于config方法来说,就是调用Config对象的load方法。这里就不多说了。helper方法也不是很复杂。无非是在APPPATH和BASEPATH的helpers目录中找到对应的helper文件,然后加载进来。加载之后,通过_ci_helpers这个属性注册一下这个helper已经加载过了,以免重复加载。
        在helper加载的时候,也有扩展helper的功能(也就是那个“MY_”前缀)。不过对于扩展helper的加载顺序地处理与其他的有所不同,Loader会先加载扩展helper,然后才是被扩展的helper:
        
helper方法的代码片段
PHP复制代码
$ext_helper = APPPATH.'helpers/'.config_item('subclass_prefix').$helper.EXT;
 
// Is this a helper extension request?                        
if (file_exists($ext_helper))
{
        $base_helper = BASEPATH.'helpers/'.$helper.EXT;
       
        if ( ! file_exists($base_helper))
        {
                show_error('Unable to load the requested file: helpers/'.$helper.EXT);
        }
       
        include_once($ext_helper);
        include_once($base_helper);
}
复制代码

        
        不过这到底是什么原因我还没搞明白。
        唠叨两句:看了一下CI的Loader,感觉有很多可以改进的地方。至少有不少功能重复地代码在来回复制粘贴,这些都是代码的坏味道。而对于Loader来说,如果从OO的角度来看,还可以再抽象出一个资源层次,来统一这些加载的脚本文件。当然,也可以考虑的层次更详细一些。

点评

Router类与HTTP请求解析 一般来说,一个CI的典型的URL是“http://hostname/index.php/welcome/index”。那么CI到底是怎么把这个请求的URL解析成welcome类的ind...  发表于 2013-7-18 15:05
 楼主| 发表于 2009-8-22 01:33:56 | 显示全部楼层
(接楼上)

        Router类与HTTP请求解析
        一般来说,一个CI的典型的URL是“http://hostname/index.php/welcome/index”。那么CI到底是怎么把这个请求的URL解析成welcome类的index方法的呢?魔法就发生在Router类里面,而这里还有另外一个帮手,就是URI。
        CI_Router类在构造的时候读入了另外两个类:Config和URI。然后就调用了_set_routing方法。
        在_set_routing方法中,首先判断的是否启用了query_string。如果启用了,就用对应的$_GET数组中对应的索引获取Controller类、方法的值,然后返回。
        而针对query_string相关的配置可以在config.php中找到:
        
针对query_string相关的配置
PHP复制代码
$config['enable_query_strings'] = FALSE;
$config['controller_trigger']         = 'c';
$config['function_trigger']         = 'm';
$config['directory_trigger']         = 'd'; // experimental not currently in use
复制代码

        
        正如第四行语句后面的注释所说,directory_trigger目前并没有使用上。
        当然上面举的URL的例子并没有使用query_string,所以往下看更有意义。接下来发生的事情就是装载routes.php这个配置文件。然后把这个文件中的$route变量的值赋给类中的routes属性。(看来针对配置文件,CI的待遇并不相同)
        下面是设置默认的Controller。之后就是调用URI类的_fetch_uri_string方法。
        
CI_URI类中的_fetch_uri_string方法
PHP复制代码
function _fetch_uri_string()
{
        if (strtoupper($this->config->item('uri_protocol')) == 'AUTO')
        {
                // If the URL has a question mark then it's simplest to just
                // build the URI string from the zero index of the $_GET array.
                // This avoids having to deal with $_SERVER variables, which
                // can be unreliable in some environments
                if (is_array($_GET) && count($_GET) == 1 && trim(key($_GET), '/') != '')
                {
                        $this->uri_string = key($_GET);
                        return;
                }
       
                // Is there a PATH_INFO variable?
                // Note: some servers seem to have trouble with getenv() so we'll test it two ways                
                $path = (isset($_SERVER['PATH_INFO'])) ? $_SERVER['PATH_INFO'] : @getenv('PATH_INFO');                        
                if (trim($path, '/') != '' && $path != "/".SELF)
                {
                        $this->uri_string = $path;
                        return;
                }
                               
                // No PATH_INFO?... What about QUERY_STRING?
                $path =  (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : @getenv('QUERY_STRING');        
                if (trim($path, '/') != '')
                {
                        $this->uri_string = $path;
                        return;
                }
               
                // No QUERY_STRING?... Maybe the ORIG_PATH_INFO variable exists?
                $path = (isset($_SERVER['ORIG_PATH_INFO'])) ? $_SERVER['ORIG_PATH_INFO'] : @getenv('ORIG_PATH_INFO');        
                if (trim($path, '/') != '' && $path != "/".SELF)
                {
                        // remove path and script information so we have good URI data
                        $this->uri_string = str_replace($_SERVER['SCRIPT_NAME'], '', $path);
                        return;
                }
 
                // We've exhausted all our options...
                $this->uri_string = '';
        }
        else
        {
                $uri = strtoupper($this->config->item('uri_protocol'));
               
                if ($uri == 'REQUEST_URI')
                {
                        $this->uri_string = $this->_parse_request_uri();
                        return;
                }
               
                $this->uri_string = (isset($_SERVER[$uri])) ? $_SERVER[$uri] : @getenv($uri);
        }
       
        // If the URI contains only a slash we'll kill it
        if ($this->uri_string == '/')
        {
                $this->uri_string = '';
        }
}
复制代码

        
        从这段不长的代码中,可以了解到CI会先判断config.php文件中设置的uri_prorocol参数,如果为AUTO的话,就按照GET、$_SERVER变量的PATH_INFO、QUERY_STRING、ORIG_PATH_INFO的顺序逐一试验,如果能通过就认定这个参数就是所要获得的URI。
        一般来说,没有GET的参数,所以CI紧接着就是测试PATH_INFO这个参数。而PATH_INFO到底是什么呢?可以在index.php中加入一行代码来看看$_SERVER这个变量。而对于上面给的例子来说,PATH_INFO会是“/welcome/index”。
        有了uri_string,回到CI_Router类中,_set_routing会先判断这个uri_string是否为空,也就是请求默认的控制器的index方法。不过CI并没有简单地把default_controller直接传入set_class方法,而是在之前用_validate_request方法过滤了一下。
        首先,default_controller字符串先用“/”explode成数组传入_validate_request方法。当_validate_request接收这个参数之后,先判断这个数组的第一个元素是否就是一个Controller文件。如果是,直接返回。否则会判断第一个元素是不是一个文件夹的名称。如果是呢,则把这个元素传入set_directory以便CodeIginter.php中对Controller进行加载时使用。不过从CI的代码来看,它只支持在Controller下面的一层目录。而两层对它来说就不行了。
        
_validate_request方法的代码片段
PHP复制代码
if (is_dir(APPPATH.'controllers/'.$segments[0]))
{                
        // Set the directory and remove it from the segment array
        $this->set_directory($segments[0]);
        $segments = array_slice($segments, 1);
       
        if (count($segments) > 0)
        {
                // Does the requested controller exist in the sub-folder?
                if ( ! file_exists(APPPATH.'controllers/'.$this->fetch_directory().$segments[0].EXT))
                {
                        show_404($this->fetch_directory().$segments[0]);
                }
        }
        else
        {
                $this->set_class($this->default_controller);
                $this->set_method('index');
       
                // Does the default controller exist in the sub-folder?
                if ( ! file_exists(APPPATH.'controllers/'.$this->fetch_directory().$this->default_controller.EXT))
                {
                        $this->directory = '';
                        return array();
                }
       
        }
 
        return $segments;
}
复制代码

        
        上面这段代码是_validate_request方法中确定了Controller是在controllers文件夹下的某个文件夹之中进行处理的代码。这段代码还有另外一个隐含的意思。举个例子就是,controllers文件夹下有个test文件夹,那么当请求“http://hostname/test/”的时候_validate_request会把test放在set_directory中,而set_class是default_controller,set_method是“index”。而这些工作就是加粗的else之后的代码块做的。
        唠叨两句:如果default_controller是“foo/bar”时,set_directory的值是foo,而set_class则是bar。如果当controllers中有个foo文件夹,而default_controller是foo的话,那么set_directory的值是foo,而set_class也是foo。
        不过上面的例子中uri_string并不是空的,那么真正运行的代码是后面的内容。
        
_set_routing方法的代码片段
PHP复制代码
unset($this->routes['default_controller']);
 
// Do we need to remove the URL suffix?
$this->uri->_remove_url_suffix();
 
// Compile the segments into an array
$this->uri->_explode_segments();
 
// Parse any custom routing that may exist
$this->_parse_routes();                
 
// Re-index the segment array so that it starts with 1 rather than 0
$this->uri->_reindex_segments();
复制代码

        
        首先是删除“default_controller”的值,然后是去掉URL前缀,这个方法从CI_URI中可以找到,代码也很简单,用正则表达式进行一个简单的替换。
        接下来是_explode_segments方法,从名字来看就是把uri_string字符串explode成一个数组。
        
CI_URI类的_explode_segments方法
PHP复制代码
function _explode_segments()
{
        foreach(explode("/", preg_replace("|/*(.+?)/*$|", "\\1", $this->uri_string)) as $val)
        {
                // Filter segments for security
                $val = trim($this->_filter_uri($val));
               
                if ($val != '')
                {
                        $this->segments[] = $val;
                }
        }
}
复制代码

        
        首先的正则表达式替换目的是把字符串首尾的“/”去掉,然后用explode方法拆成数组。然后针对数组的每个元素对其过滤一下。最后把过滤过的值放到segments数组中去。当在过滤数组的元素是,一旦过滤方法觉得这个值有问题,CI会直接退出。
        接下来的重头戏就是_parse_routes方法了。不过在配置文件中,并没有其他的routes规则,而且在之前已经删除了$this->routes中的default_controller变量,那么现在的$this->routes数组中只有一个元素了。那么在_parse_routes方法中,只有上面的几行起作用:
        
_parse_routes方法的代码片段
PHP复制代码
// Do we even have any custom routing to deal with?
// There is a default scaffolding trigger, so we'll look just for 1
if (count($this->routes) == 1)
{
        $this->_set_request($this->uri->segments);
        return;
}
复制代码

        
        接下来就是到_set_request方法中看看发生了什么。首先是调用_validate_request方法,这里就不重复说了,接下来就是根据$segment数组中的第0号元素和第1号元素来设置Controller类名称和方法名称。接下来回到_set_routing方法,最后一行代码是调用CI_URI的_reindex_segments方法,这个方法就是把CI_URI类中的resegments和segments两个数组的下标从1开始。
        唠叨两句:可能是看源代码不够所以,目前还没搞明白这个reindex到底能起到什么作用。
        如果请求的URL是“http://hostname/welcome/index/1”的时候,CI_URI对象中的segments数组中会包含3个元素。而回忆一下在CodeIgniter.php文件中调用Controller方法的代码:
        call_user_func_array(array(&$CI, $method), array_slice($URI->rsegments, 2));
        这样后面的1就当成了一个参数传入了Welcome类的index方法中。

        关于Controller类

        在CI中,Controller本身也很简单,一般来说,看看_ci_initialize方法就可以了。在_ci_initialize方法中,先是设定了一个默认加载的库名称列表,然后用load_class函数逐一加载。接下来就是对autoload.php文件中设置的自动加载的库进行加载。
        而在进行自动加载时,会有针对PHP 4和PHP 5的两段代码。对于PHP 5来说,加载Loader库,然后调用CI_Loader的_ci_autoload方法即可,而对于PHP 4来说,在Base4.php中,CI_Base本身就是集成了CI_Loader,所以直接调用自己的_ci_autoload即可。

        关于缓存

        CI把缓存系统也整合到整个MVC的过程当中了,而且负责缓存工作的类是CI_Output类。那么CI的缓存到底是怎么设计的呢?仔细看看CI_Output类的3个方法就可以了:_display、_write_cache、_display_cache。
        先来看看_display这个方法。如果之前我们调用了CI_Output实例的cache方法,传入一个比0大的数字的话,_display方法就会调用_write_cache方法把当前的输出内容写入缓存——也就是文件:
        
CI_Output类的_display方法的代码片段
PHP复制代码
// Do we need to write a cache file?
if ($this->cache_expiration > 0)
{
        $this->_write_cache($output);
}
复制代码

        
        在_write_cache方法中,CI需要从配置文件中得到缓存文件存放的路径,如果没有设置,默认的路径是BASEPATH/cache文件夹。当然,为了文件能正常写入,CI会检测设置的文件夹是否存在,并且可以写入文件。
        之后呢,就是设置缓存文件的文件名了,这段代码很容易,就是配置文件中的base_url、index_page、和uri_string这三个字符串相加,然后用md5散列一下。接下来是设置过期时间,由于cache方法设置的时间是以分为单位的,所以这里需要换算成秒。
        最后就是写入缓存文件了,文件的内容布局很特别,如果缓存的内容是“abcde”,且过期时间($expire变量的值)是3600的话,缓存文件的内容是:
        3600TS--->abcde
        还记得在CodeIgniter.php文件中输出缓存的那段代码吗?调用的是CI_Output实例的_display_cache方法。这个方法的思路与_write_cache相同,就是把写文件改为读取文件,把设置缓存时间改为判断缓存是否过期。而对于:
        3600TS--->abcde
        这样的文件内容,CI用了正则表达式来获取TS之前的值。如果判断这个缓存已经过期,那么就删除这个文件。

        在最后想说的

        初次接触CI觉得很简洁,不过看过这么一段CI的代码之后,对其中的设计有个更深的了解。但凭良心说,CI的代码本身的“味道”并不很好。不过CI本身并不复杂,而且扩展能力也很强,而透过代码来寻找作者的意图也是一件挺有意思的事情,不是吗?
        对于在Zend Studio中调试CI的项目如果对CI本身不做任何“动作”的话是不可能的。其实原因都出在了Router这个库里面了。Zend Studio调试时候本身会设置一些Router需要检查的参数,如GET,PATH_INFO。要想让CI在Zend Studio中正常跑起来,甚至是测试一些控制器,那么改改index.php文件吧。其实很简单,我的办法是:
        
在index.php中加的代码
PHP复制代码
$_SERVER['PATH_INFO'] = "welcome/index";
$_SERVER['QUERY_STRING'] = "";
$_GET = array();
复制代码


        注意,上面3行代码一定要在加载CodeIgniter.php文件之前写。
        对于CI的一个分支Kohana,听说这是个纯粹的PHP 5框架,而且OO做的更好一些。不过还没有深入地去看他的代码,到底是个什么样子确实也很让人向往。
发表于 2009-8-22 15:37:04 | 显示全部楼层
哈哈,来做沙发了,认真的看文章。。
发表于 2009-8-22 17:07:06 | 显示全部楼层
坐板凳,慢慢看唠!
发表于 2009-8-23 09:03:54 | 显示全部楼层
得好好看看 ~
发表于 2009-8-23 20:36:27 | 显示全部楼层
很好,很强大
发表于 2009-8-24 17:40:31 | 显示全部楼层
先做标记, 教程看没看完
发表于 2009-8-26 09:59:23 | 显示全部楼层
这篇是不是需要先对CI使用上有个程度后再看啊?我现在看怎么看不懂写的是什么?
 楼主| 发表于 2009-8-26 10:26:43 | 显示全部楼层
这篇是不是需要先对CI使用上有个程度后再看啊?我现在看怎么看不懂写的是什么?
kazaff 发表于 2009-8-26 09:59

教程索引里,表明是中级教程了,不是入门教程,呵呵

本版积分规则