CodeIgniter 源代码解析

来源:互联网 发布:linux上装eclipse 编辑:程序博客网 时间:2024/06/10 03:01
接触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方面的一个“败笔”吧。
0 0
原创粉丝点击