|
楼主 |
发表于 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的角度来看,还可以再抽象出一个资源层次,来统一这些加载的脚本文件。当然,也可以考虑的层次更详细一些。 |
|