从本篇开始,基本上算是深入到了CI框架的内部,下面就让我们一步步去探索这个框架的实现、结构和设计。
Common.php文件定义了一系列的全局函数(一般来说,全局函数具有最高的加载优先权,因此大多数的框架中BootStrap引导文件都会最先引入全局函数,以便于之后的处理工作)。
CI框架全局函数库文件Common.php中所有全局函数的定义方式都为:
if (!function_exists('func_name')) { function func_name() { #function body.... } }
这样做,是为了防止定义重名函数(之后如果我们要定义系统的全局函数,也都可以使用这种定义方式)。
下面,我们就对每个函数进行逐个展开解析:
1、is_php()
if (!function_exists('is_php')) { //判断当前php版本是不是$version以上的。调用version_compare()这个函数。 function is_php($version) { static $_is_php; $version = (string)$version; if (!isset($_is_php[$version])) { //PHP_VERION能够获得当前php版本。 $_is_php[$version] = version_compare(PHP_VERSION, $version, '>='); } return $_is_php[$version]; } }
这个函数的命名很明显,就是判断当前环境的PHP版本是否是特定的PHP版本(或者高于该版本)
该函数内部有一个static的$_is_php数组变量,用于缓存结果(因为在特定的运行环境中,PHP的版本是已知的且是不变的,所以通过缓存的方式,避免每次调用时都去进行version_compare。这种方式,与一般的分布式缓存(如Redis)的处理思维是一致的,不同的是,这里是使用static数组的方式,而分布式缓存大多使用内存缓存)。
为什么要定义这个函数呢?这是因为,CI框架中有一些配置依赖于PHP的版本和行为(如magic_quotes,PHP 5.3版本之前,该特性用于指定是否开启转义,而PHP5.3之后,该特性已经被废弃)。这就好比是针对不同的浏览器进行Css Hack一样(这里仅仅是比喻,实际上,PHP并没有太多的兼容性问题)。
2、is_really_writable()
//文件的写入性能测试 if (!function_exists('is_really_writable')) { //该函数和php官方手册上面写的差不多,兼容linux/Unix和windows系统。 //可以查看手册:http://www.php.net/manual/en/function.is-writable.php function is_really_writable($file) { //DIRECTORY_SEPARATOR是系统的目录分割符。利用它的值可以知道当前是不是linux系统。 if (DIRECTORY_SEPARATOR === '/' && (is_php('5.4') OR !ini_get('safe_mode'))) { //如果是linux系统的话,那么可以直接调用此方法来判断文件是否可写。 return is_writable($file); } //如果是windows系统,则尝试写入一个文件来判断。 if (is_dir($file)) { //如果是目录,则创建一个随机命名的文件。 $file = rtrim($file, '/') . '/' . md5(mt_rand()); //如果文件不能创建,则返回不可写。 if (($fp = @fopen($file, 'ab')) === FALSE) { return FALSE; } fclose($fp); //删除刚才的文件。 @chmod($file, 0777); @unlink($file); return TRUE; } elseif (!is_file($file) OR ($fp = @fopen($file, 'ab')) === FALSE) { //如果是一个文件,而通过写入方式打不开,则返回不可写。 return FALSE; } fclose($fp); return TRUE; } }
这个函数用于判断文件或者目录是否真实可写,一般情况下,通过内置函数is_writable()返回的结果是比较可靠的,但是也有一些例外,比如:
Ⅰ Windows中,如果对文件或者目录设置了只读属性,则is_writable返回结果是true,但是却无法写入。
Ⅱ Linux系统中,如果开启了Safe Mode,则也会影响is_writable的结果。
因此,本函数的处理是:如果是一般的Linux系统且没有开启safe mode,则直接调用is_writable,否则:如果是目录,则尝试在目录中创建一个文件来检查目录是否可写,如果是文件,则尝试以写入模式打开文件,如果无法打开,则返回false。注意,即使是使用fopen检查文件是否可写,也一定记得调用fclose关闭句柄,这是一个好的习惯。
3、load_class()
if (!function_exists('load_class')) { //加载类。默认是加载libraries里面的,如果要加载核心组件,$directory就为'core' function &load_class($class, $directory = 'libraries', $param = NULL) { //用一个静态数组,保存已经加载过的类的实例,防止多次实例消耗资源,实现单例化。 static $_classes = array(); //如果已经保存在这里,就返回它。 if (isset($_classes[$class])) { return $_classes[$class]; } $name = FALSE; //这里,如果应用目录下有和系统目录下相同的类的话,优先引入应用目录,也就是你自己定义的。 foreach (array(APPPATH, BASEPATH) as $path) { if (file_exists($path . $directory . '/' . $class . '.php')) { $name = 'CI_' . $class; if (class_exists($name, FALSE) === FALSE) { require_once($path . $directory . '/' . $class . '.php'); } break; } } if (file_exists(APPPATH . $directory . '/' . config_item('subclass_prefix') . $class . '.php')) { $name = config_item('subclass_prefix') . $class; if (class_exists($name, FALSE) === FALSE) { require_once(APPPATH . $directory . '/' . $name . '.php'); } } if ($name === FALSE) { set_status_header(503); echo 'Unable to locate the specified class: ' . $class . '.php'; exit(5); // EXIT_UNK_CLASS } //这个函数只是用来记录已经被加载过的类的类名而已。 is_loaded($class); $_classes[$class] = isset($param) ? new $name($param) : new $name(); return $_classes[$class]; } }
这个函数有几个特殊的地方需要重点关注:
Ⅰ 注意这个函数的签名,function &load_class( $class,$directory,$prefix).看到前面那个特殊的&符号没?没错,这个函数返回的是一个class实例的引用. 对该实例的任何改变,都会影响下一次函数调用的结果。
Ⅱ 这个函数也有一个内部的static变量缓存已经加载的类的实例,实现方式类似于单例模式(Singleton)。
Ⅲ 函数优先查找APPPATH和BASEPATH中查找类,然后才从$directory中查找类,这意味着,如果directory中存在着同名的类(指除去前缀之后同名),CI加载的实际上是该扩展类。这也意味着,可以对CI的核心进行修改或者扩展。
4、is_loaded()
//跟踪已加载库的轨道。这个函数被调用的函数在load_class() if (!function_exists('is_loaded')) { //记录有哪些类是已经被加载的。 function &is_loaded($class = '') { static $_is_loaded = array(); if ($class !== '') { $_is_loaded[strtolower($class)] = $class; } return $_is_loaded; } }
这个函数用于追踪所有已加载的class,代码比较简洁。
5、get_config()、config_item()、get_mimes()
① get_config()
//加载主config.php文件,这个功能可以让我们抓住的配置文件,即使配置类还没有被实例化。 if (!function_exists('get_config')) { function &get_config(Array $replace = array()) { static $config; if (empty($config)) { $file_path = APPPATH . 'config/config.php'; $found = FALSE; if (file_exists($file_path)) { $found = TRUE; require($file_path); } if (file_exists($file_path = APPPATH . 'config/' . ENVIRONMENT . '/config.php')) { require($file_path); } elseif (!$found) { set_status_header(503); echo 'The configuration file does not exist.'; exit(3); // EXIT_CONFIG } if (!isset($config) OR !is_array($config)) { set_status_header(503); echo 'Your config file does not appear to be formatted correctly.'; exit(3); // EXIT_CONFIG } } foreach ($replace as $key => $val) { $config[$key] = $val; } return $config; } }
这个函数用于加载主配置文件(即位于config/目录下的config.php文件,如果定义了针对特定ENVIRONMENT的config.php文件,则是该文件)。
有几个需要注意的点:
Ⅰ 函数只加载主配置文件,而不会加载其他配置文件(这意味着,如果你添加了其他的配置文件,在框架预备完毕之前,不会读取你的配置文件)。在Config组件实例化之前,所有读取主配置文件的工作都由该函数完成。
Ⅱ 该函数支持动态运行的过程中修改Config.php中的条目(配置信息只可能修改一次,因为该函数也有static变量做缓存,若缓存存在,则直接返回配置)。
Ⅲ Return $_config[0] = & $config。是config文件中$config的引用,防止改变Config的配置之后,由于该函数的缓存原因,无法读取最新的配置。
② config_item()
if (!function_exists('config_item')) { //取得配置数组中某个元素。 function config_item($item) { static $_config; if (empty($_config)) { //引用不能直接分配给静态变量,所以我们使用一个数组 $_config[0] =& get_config(); } return isset($_config[0][$item]) ? $_config[0][$item] : NULL; } }
这个函数调用了load_config,并获取相应的设置条目,代码比较简洁。
③ get_mimes()
if (!function_exists('get_mimes')) { //获得mimes.phpMIME类型的数组 //此函数是最新添加 function &get_mimes() { static $_mimes; if (empty($_mimes)) { if (file_exists(APPPATH . 'config/' . ENVIRONMENT . '/mimes.php')) { $_mimes = include(APPPATH . 'config/' . ENVIRONMENT . '/mimes.php'); } elseif (file_exists(APPPATH . 'config/mimes.php')) { $_mimes = include(APPPATH . 'config/mimes.php'); } else { $_mimes = array(); } } return $_mimes; } }
此函数返回从配置/ mimes.php MIME类型的数组,代码比较简洁。一般用不到此函数。
6、is_https()、is_cli()
① is_https()
if (!function_exists('is_https')) { //此函数也是最新添加 function is_https() { if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off') { return TRUE; } elseif (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') { return TRUE; } elseif (!empty($_SERVER['HTTP_FRONT_END_HTTPS']) && strtolower($_SERVER['HTTP_FRONT_END_HTTPS']) !== 'off') { return TRUE; } return FALSE; } }
这个函数确定应用程序是否是通过一个加密(HTTPS)连接访问。代码较为简洁。
② is_cli()
if (!function_exists('is_cli')) { //此函数也是新添加,我们在开发过程中一般用不到 function is_cli() { //PHP_SAPI 描述 PHP 所使用的接口类型 return (PHP_SAPI === 'cli' OR defined('STDIN')); } }
这个函数用于测试,看看是否有一个命令是由命令行。
以上两个函数在我们的开发过程中基本用不到。了解即可。
7、show_error()、show_404()
① show_error()
if (!function_exists('show_error')) { function show_error($message, $status_code = 500, $heading = 'An Error Was Encountered') { $status_code = abs($status_code); if ($status_code < 100) { $exit_status = $status_code + 9; // 9 is EXIT__AUTO_MIN if ($exit_status > 125) // 125 is EXIT__AUTO_MAX { $exit_status = 1; // EXIT_ERROR } $status_code = 500; } else { $exit_status = 1; // EXIT_ERROR } $_error =& load_class('Exceptions', 'core'); echo $_error->show_error($heading, $message, 'error_general', $status_code); exit($exit_status); } }
这是CI定义的可以用来展示错误信息的函数,该函数使用了Exceptions组件(之后我们将看到,CI中都是通过Exceptions组件来管理错误的)来处理错误。注意该函数不仅仅是显示错误,而且会终止代码的执行(exit)。
② show_404()
if (!function_exists('show_404')) { function show_404($page = '', $log_error = TRUE) { $_error =& load_class('Exceptions', 'core'); $_error->show_404($page, $log_error); exit(4); // EXIT_UNKNOWN_FILE } }
这个函数没有太多解释的东西,返回404页面。
8、log_message()
if (!function_exists('log_message')) { //记录日志 function log_message($level, $message) { static $_log; if ($_log === NULL) { $_log[0] =& load_class('Log', 'core'); } $_log[0]->write_log($level, $message); } }
调用Log组件记录log信息,类似Debug。需要注意的是,如果主配置文件中log_threshold被设置为0,则不会记录任何Log信息。
9、set_status_header()
//CI框架允许你设置HTTP协议的头信息 if (!function_exists('set_status_header')) { function set_status_header($code = 200, $text = '') { if (is_cli()) { return; } //如果调用此函数本身出错,则发出一个错误。 if (empty($code) OR !is_numeric($code)) { show_error('Status codes must be numeric', 500); } //此函数构造一个响应头。$stati为响应码与其响应说明。 //如果$text不为空,一般是因为调用此函数时,给的响应码不正确同时又没有写出响应报文信息。 if (empty($text)) { is_int($code) OR $code = (int)$code; $stati = array( 100 => 'Continue', 101 => 'Switching Protocols', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 307 => 'Temporary Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Timeout', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Long', 415 => 'Unsupported Media Type', 416 => 'Requested Range Not Satisfiable', 417 => 'Expectation Failed', 422 => 'Unprocessable Entity', 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported' ); if (isset($stati[$code])) { $text = $stati[$code]; } else { show_error('No status text available. Please check your status code number or supply your own message text.', 500); } } //php_sapi_name()方法可以获得PHP与服务器之间的接口类型, //下面是以cgi类型和以服务器模块形式类型的不同发出响应的方式。 if (strpos(PHP_SAPI, 'cgi') === 0) { header('Status: ' . $code . ' ' . $text, TRUE); } else { //取得当前协议。 $server_protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1'; header($server_protocol . ' ' . $code . ' ' . $text, TRUE, $code); } } }
CI框架允许你设置HTTP协议的头信息。设置方法为:$this->output->set_status_header(“401”,“hehehe”);(CI的Output组件暴露了set_status_header()对外接口,该接口即是调用set_status_header函数)。
值得注意的是,现在很多服务器内部扩展加入了自定义的状态码,如nginx:
ngx_string(ngx_http_error_495_page), /* 495, https certificate error */
ngx_string(ngx_http_error_496_page), /* 496, https no certificate */
ngx_string(ngx_http_error_497_page), /* 497, http to https */
ngx_string(ngx_http_error_404_page), /* 498, canceled */
ngx_null_string, /* 499, client has closed connection */所以你在查看服务器的error_log时,如果看到了比较诡异的错误状态码,不要惊慌,这不是bug. 这也说明,如果你要自定义自己的状态码和状态码描述文案,可以在该函数的内部$stati变量中添加自定义的状态码和文案。更多详细的内容,可以查看header函数的manual。
10、_error_handler()、_exception_handler()、_shutdown_handler()
① _error_handler()
if (!function_exists('_error_handler')) { function _error_handler($severity, $message, $filepath, $line) { $is_error = (((E_ERROR | E_COMPILE_ERROR | E_CORE_ERROR | E_USER_ERROR) & $severity) === $severity); if ($is_error) { set_status_header(500); } //注意下面的符号是&而不是&& if (($severity & error_reporting()) !== $severity) { return; } $_error =& load_class('Exceptions', 'core'); $_error->log_exception($severity, $message, $filepath, $line); //如果符合则交给Exception组件的show_php_error();进行处理 if (str_ireplace(array('off', 'none', 'no', 'false', 'null'), '', ini_get('display_errors'))) { $_error->show_php_error($severity, $message, $filepath, $line); } //下面两行只是根据配置文件判断要不要log错误信息而已。 if ($is_error) { exit(1); // EXIT_ERROR } } }
这个函数和下个函数放在一起解析。
② _exception_handler()
if (!function_exists('_exception_handler')) { //发送给记录器未捕获的异常并显示只有display_errors是使他们不出现在生产环境中。 function _exception_handler($exception) { $_error =& load_class('Exceptions', 'core'); $_error->log_exception('error', 'Exception: ' . $exception->getMessage(), $exception->getFile(), $exception->getLine()); if (str_ireplace(array('off', 'none', 'no', 'false', 'null'), '', ini_get('display_errors'))) { $_error->show_exception($exception); } exit(1); } }
这个函数会根据当前设置的error_reporting的设置和配置文件中threshold的设置来决定PHP错误的显示和记录。在CI中,这个函数是作为set_error_handler的callback, 来代理和拦截PHP的错误信息(PHP手册中明确指出:以下级别的错误不能由用户定义的函数来处理: E_ERROR、 E_PARSE、 E_CORE_ERROR、 E_CORE_WARNING、 E_COMPILE_ERROR、 E_COMPILE_WARNING,和在 调用 set_error_handler() 函数所在文件中产生的大多数 E_STRICT 。同样,如果在set_error_handler调用之前发生的错误,也无法被_exception_handler捕获,因为在这之前,_exception_handler尚未注册)。
E_STRICT是PHP5中定义的错误级别,是严格语法模式的错误级别,并不包含在E_STRICT. 由于E_STRICT级别的错误可能会很多,因此,CI的做法是,忽略这类错误。
③ _shutdown_handler()
//关机程序 if (!function_exists('_shutdown_handler')) { //这是停机处理,在codeigniter.php顶部声明。 //使用这个的主要原因是模拟一个完整的自定义异常处理程序。 function _shutdown_handler() { $last_error = error_get_last(); if (isset($last_error) && ($last_error['type'] & (E_ERROR | E_PARSE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING)) ) { _error_handler($last_error['type'], $last_error['message'], $last_error['file'], $last_error['line']); } } }
这个是停机程序,代码也是比较简洁,没什么好说的。
11、remove_invisible_characters()、html_escape()、_stringify_attributes()
① remove_invisible_characters()
if (!function_exists('remove_invisible_characters')) { //这可以防止夹空字符之间的ASCII字符,如java \ 0script。 function remove_invisible_characters($str, $url_encoded = TRUE) { $non_displayables = array(); if ($url_encoded) { $non_displayables[] = '/%0[0-8bcef]/i'; // url encoded 00-08, 11, 12, 14, 15 $non_displayables[] = '/%1[0-9a-f]/i'; // url encoded 16-31 } $non_displayables[] = '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S'; // 00-08, 11, 12, 14-31, 127 do { $str = preg_replace($non_displayables, '', $str, -1, $count); } while ($count); return $str; } }
这个函数的含义非常明确,就是去除字符串中的不可见字符。这些不可见字符包括:ASCII码表中的00-31,127(保留09,10,13,分别为tab,换行和回车换行,这些虽然不可见,但却是格式控制字符)。然后通过正则替换去除不可见字符。
理论上将,preg_replace会替换所有的满足正则表达式的部分,这里使用while循环的理由是:可以去除嵌套的不可见字符。如 %%0b0c。如果只执行一次替换的话,剩余的部分%0c依然是不可见字符,所以要迭代去除($count返回替换的次数)。② html_escape()
if (!function_exists('html_escape')) { function html_escape($var, $double_encode = TRUE) { if (empty($var)) { return $var; } if (is_array($var)) { foreach (array_keys($var) as $key) { $var[$key] = html_escape($var[$key], $double_encode); } return $var; } return htmlspecialchars($var, ENT_QUOTES, config_item('charset'), $double_encode); } }
这个函数,实际上是数组中的元素递归调用htmlspecialchars。
③ _stringify_attributes()
if (!function_exists('_stringify_attributes')) { function _stringify_attributes($attributes, $js = FALSE) { $atts = NULL; if (empty($attributes)) { return $atts; } if (is_string($attributes)) { return ' ' . $attributes; } $attributes = (array)$attributes; foreach ($attributes as $key => $val) { $atts .= ($js) ? $key . '=' . $val . ',' : ' ' . $key . '="' . $val . '"'; } return rtrim($atts, ','); } }
这个函数stringify使用HTML标签属性,用于将字符串、数组或属性的对象转换为字符串的辅助函数。
总结一下,Common.php是在各组件加载之前定义的一系列全局函数。这些全局函数的作用是获取配置、跟踪加载class、安全性过滤等。而这么做的目的之一,就是避免组件之间的过多依赖。
最后,贴一下整个Common.php文件的源码(注释版):
<?php /** * ======================================= * Created by Pocket Knife Technology. * User: ZhiHua_W * Date: 2016/10/17 0056 * Time: 上午 11:14 * Project: CodeIgniter框架—源码分析 * Power: Analysis for Common.php * ======================================= */ /** * 这个BASEPATH,就是在入口文件(index.php)里面定义的那个BASEPATH * 如果没有定义BASEPATH,那么直接退出,下面程序都不执行。 * 此句为必须,以后就不再注释解释 */ defined('BASEPATH') OR exit('No direct script access allowed'); /** * 全局公共函数库 */ /** * 为什么还要定义这些全局函数呢? * 比如说,下面有很多函数,如get_config()、config_item()这两个方法不是应该由core/Config.php这个组件去做么? * 那个load_class()不应该由core/Loader.php去做么? * 把这些函数定义出来貌似感觉架构变得不那么优雅,有点多余。 * 其实是出于这样一种情况: * 比如说,如果一切和配置有关的动作都由Config组件来完成,一切加载的动作都由Loader来完成, * 试想一下,如果我要加载Config组件,那么,必须得通过Loader来加载,所以Loader必须比Config要更早实例化, * 但是如果Loader实例化的时候需要一些和Loader有关的配置信息才能实例化呢?那就必须通过Config来为它取得配置信息。 * 这里就出现了鸡和鸡蛋的问题。 * 我之前写自己的框架也纠结过这样的问题,后来参考了YII框架,发现它里面其实都有同样的问题,它里面有个Exception的组件, * 但是在加载这个Exception组件之前,在加载其它组件的时候,如果出错了,那谁来处理异常和错误信息呢?答案就是先定义一些公共的函数。 * 所以这些公共函数就很好地解决了这个问题,这也是为什么Common.php要很早被引入。 */ //确定最新的PHP版本大于该值 if (!function_exists('is_php')) { //判断当前php版本是不是$version以上的。调用version_compare()这个函数。 function is_php($version) { static $_is_php; $version = (string)$version; if (!isset($_is_php[$version])) { //PHP_VERION能够获得当前php版本。 $_is_php[$version] = version_compare(PHP_VERSION, $version, '>='); } return $_is_php[$version]; } } //文件的写入性能测试 if (!function_exists('is_really_writable')) { //该函数和php官方手册上面写的差不多,兼容linux/Unix和windows系统。 //可以查看手册:http://www.php.net/manual/en/function.is-writable.php function is_really_writable($file) { //DIRECTORY_SEPARATOR是系统的目录分割符。利用它的值可以知道当前是不是linux系统。 if (DIRECTORY_SEPARATOR === '/' && (is_php('5.4') OR !ini_get('safe_mode'))) { //如果是linux系统的话,那么可以直接调用此方法来判断文件是否可写。 return is_writable($file); } //如果是windows系统,则尝试写入一个文件来判断。 if (is_dir($file)) { //如果是目录,则创建一个随机命名的文件。 $file = rtrim($file, '/') . '/' . md5(mt_rand()); //如果文件不能创建,则返回不可写。 if (($fp = @fopen($file, 'ab')) === FALSE) { return FALSE; } fclose($fp); //删除刚才的文件。 @chmod($file, 0777); @unlink($file); return TRUE; } elseif (!is_file($file) OR ($fp = @fopen($file, 'ab')) === FALSE) { //如果是一个文件,而通过写入方式打不开,则返回不可写。 return FALSE; } fclose($fp); return TRUE; } } //加载类注册 if (!function_exists('load_class')) { //加载类。默认是加载libraries里面的,如果要加载核心组件,$directory就为'core' function &load_class($class, $directory = 'libraries', $param = NULL) { //用一个静态数组,保存已经加载过的类的实例,防止多次实例消耗资源,实现单例化。 static $_classes = array(); //如果已经保存在这里,就返回它。 if (isset($_classes[$class])) { return $_classes[$class]; } $name = FALSE; //这里,如果应用目录下有和系统目录下相同的类的话,优先引入应用目录,也就是你自己定义的。 foreach (array(APPPATH, BASEPATH) as $path) { if (file_exists($path . $directory . '/' . $class . '.php')) { $name = 'CI_' . $class; if (class_exists($name, FALSE) === FALSE) { require_once($path . $directory . '/' . $class . '.php'); } break; } } //这里就用到的前缀扩展,如果在应用目录相应的目录下,有自己写的一些对CI库的扩展,那么我们加载的是它,而不是 //原来的。因为我们写的扩展是继承了CI原来的。 //所以可以看出,即使是CI的核心组件(core/下面的)我们都可以为之进行扩展。 if (file_exists(APPPATH . $directory . '/' . config_item('subclass_prefix') . $class . '.php')) { $name = config_item('subclass_prefix') . $class; if (class_exists($name, FALSE) === FALSE) { require_once(APPPATH . $directory . '/' . $name . '.php'); } } if ($name === FALSE) { //这里用的是exit();来提示错误,而不是用show_error();这是因为这个load_class的错误有可能 //在加载Exception组件之前发。 set_status_header(503); echo 'Unable to locate the specified class: ' . $class . '.php'; exit(5); // EXIT_UNK_CLASS } //这个函数只是用来记录已经被加载过的类的类名而已。 is_loaded($class); $_classes[$class] = isset($param) ? new $name($param) : new $name(); return $_classes[$class]; } } //跟踪已加载库的轨道。这个函数被调用的函数在load_class() if (!function_exists('is_loaded')) { //记录有哪些类是已经被加载的。 function &is_loaded($class = '') { static $_is_loaded = array(); if ($class !== '') { $_is_loaded[strtolower($class)] = $class; } return $_is_loaded; } } //加载主config.php文件,这个功能可以让我们抓住的配置文件,即使配置类还没有被实例化。 if (!function_exists('get_config')) { //这个是读取配置信息的函数,在Config类被实例化之前,由它暂负责。 //而在Config类被实例化之前,我们需要读取的配置信息,其实仅仅是config.php这个主配置文件的。 //所以这个方法是不能读出config/下其它配置文件的信息的。 //这个$replace参数,是提供一个临时替换配置信息的机会,仅一次,因为执行一次后, //配置信息都会保存在静态变量$_config中,不能改变。 function &get_config(Array $replace = array()) { static $config; if (empty($config)) { $file_path = APPPATH . 'config/config.php'; $found = FALSE; if (file_exists($file_path)) { $found = TRUE; require($file_path); } if (file_exists($file_path = APPPATH . 'config/' . ENVIRONMENT . '/config.php')) { require($file_path); } elseif (!$found) { set_status_header(503); echo 'The configuration file does not exist.'; exit(3); // EXIT_CONFIG } if (!isset($config) OR !is_array($config)) { set_status_header(503); echo 'Your config file does not appear to be formatted correctly.'; exit(3); // EXIT_CONFIG } } foreach ($replace as $key => $val) { $config[$key] = $val; } return $config; } } //返回指定的配置项 if (!function_exists('config_item')) { //取得配置数组中某个元素。 function config_item($item) { static $_config; if (empty($_config)) { //引用不能直接分配给静态变量,所以我们使用一个数组 $_config[0] =& get_config(); } return isset($_config[0][$item]) ? $_config[0][$item] : NULL; } } //返回从配置/ mimes.php MIME类型的数组 if (!function_exists('get_mimes')) { //获得mimes.phpMIME类型的数组 //此函数是最新添加 function &get_mimes() { static $_mimes; if (empty($_mimes)) { if (file_exists(APPPATH . 'config/' . ENVIRONMENT . '/mimes.php')) { $_mimes = include(APPPATH . 'config/' . ENVIRONMENT . '/mimes.php'); } elseif (file_exists(APPPATH . 'config/mimes.php')) { $_mimes = include(APPPATH . 'config/mimes.php'); } else { $_mimes = array(); } } return $_mimes; } } //确定应用程序是否是通过一个加密(HTTPS)连接访问。 if (!function_exists('is_https')) { //此函数也是最新添加 function is_https() { if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off') { return TRUE; } elseif (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') { return TRUE; } elseif (!empty($_SERVER['HTTP_FRONT_END_HTTPS']) && strtolower($_SERVER['HTTP_FRONT_END_HTTPS']) !== 'off') { return TRUE; } return FALSE; } } //测试,看看是否有一个命令是由命令行。 if (!function_exists('is_cli')) { //此函数也是新添加,我们在开发过程中一般用不到 function is_cli() { //PHP_SAPI 描述 PHP 所使用的接口类型 return (PHP_SAPI === 'cli' OR defined('STDIN')); } } //这里的show_error和下面的show_404以及 _exception_handler这三个错误的处理,实质都是由Exception组件完成的。 //详见core/Exception.php. if (!function_exists('show_error')) { function show_error($message, $status_code = 500, $heading = 'An Error Was Encountered') { $status_code = abs($status_code); if ($status_code < 100) { $exit_status = $status_code + 9; // 9 is EXIT__AUTO_MIN if ($exit_status > 125) // 125 is EXIT__AUTO_MAX { $exit_status = 1; // EXIT_ERROR } $status_code = 500; } else { $exit_status = 1; // EXIT_ERROR } $_error =& load_class('Exceptions', 'core'); echo $_error->show_error($heading, $message, 'error_general', $status_code); exit($exit_status); } } //这个函数功能类似于上面show_error()功能,而不是标准的错误模板显示404错误。 if (!function_exists('show_404')) { function show_404($page = '', $log_error = TRUE) { $_error =& load_class('Exceptions', 'core'); $_error->show_404($page, $log_error); exit(4); // EXIT_UNKNOWN_FILE } } if (!function_exists('log_message')) { //调用Log组件记录log信息,类似Debug。需要注意的是,如果主配置文件中log_threshold被设置为0,则不会记录任何Log信息 function log_message($level, $message) { static $_log; if ($_log === NULL) { $_log[0] =& load_class('Log', 'core'); } $_log[0]->write_log($level, $message); } } //CI框架允许你设置HTTP协议的头信息 if (!function_exists('set_status_header')) { function set_status_header($code = 200, $text = '') { if (is_cli()) { return; } //如果调用此函数本身出错,则发出一个错误。 if (empty($code) OR !is_numeric($code)) { show_error('Status codes must be numeric', 500); } //此函数构造一个响应头。$stati为响应码与其响应说明。 //如果$text不为空,一般是因为调用此函数时,给的响应码不正确同时又没有写出响应报文信息。 if (empty($text)) { is_int($code) OR $code = (int)$code; $stati = array( 100 => 'Continue', 101 => 'Switching Protocols', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 307 => 'Temporary Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Timeout', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Long', 415 => 'Unsupported Media Type', 416 => 'Requested Range Not Satisfiable', 417 => 'Expectation Failed', 422 => 'Unprocessable Entity', 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported' ); if (isset($stati[$code])) { $text = $stati[$code]; } else { show_error('No status text available. Please check your status code number or supply your own message text.', 500); } } //php_sapi_name()方法可以获得PHP与服务器之间的接口类型, //下面是以cgi类型和以服务器模块形式类型的不同发出响应的方式。 if (strpos(PHP_SAPI, 'cgi') === 0) { header('Status: ' . $code . ' ' . $text, TRUE); } else { //取得当前协议。 $server_protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1'; header($server_protocol . ' ' . $code . ' ' . $text, TRUE, $code); } } } //错误操作 if (!function_exists('_error_handler')) { //在CodeIgniter.php中执行set_error_handler('_exception_handler');后,以后一切非致命(非fatal)错误信息都由它处理。 //触发错误的时候,会产生几个参数,错误级别(号),错误信息,错误文件,错误行。 function _error_handler($severity, $message, $filepath, $line) { $is_error = (((E_ERROR | E_COMPILE_ERROR | E_CORE_ERROR | E_USER_ERROR) & $severity) === $severity); //有关错误等级的内容可看手册 //E_STRICT对于大多数情况来说都是没多大作用的错误提示,这里CI把它屏蔽掉,如果实在要查看,可以查看日志文件。 if ($is_error) { set_status_header(500); } //注意下面的符号是&而不是&&,php的错误等级的值都是有规律的,例如1,2,4,8...(1,10,100,1000)等等,实际上,php是通过位运算来实现的, //使得错误控制更精准。(类似linux的权限控制,rwx) //在设置error_reporting()的时候,可通过E_XX|E_YY|E_ZZ的形式来设置,而判断的时候则通过E_XX&error_repoorting()来判断 //E_XX有没有设置。例如1,10,100,1000相或|,则值为1111,则以后1,10,100,1000中任意一个与1111相&,值都为它本身。 //而E_ALL可以看到是除E_STRICT之外其它等级的“或(|)运算”。个人理解,之所以E_ALL的值是不同版本有所不同的,是 //因为有时候会加入新的错误级别,从而导致这个E_ALL的值也不一样。 if (($severity & error_reporting()) !== $severity) { return; } $_error =& load_class('Exceptions', 'core'); $_error->log_exception($severity, $message, $filepath, $line); //如果符合则交给Exception组件的show_php_error();进行处理 if (str_ireplace(array('off', 'none', 'no', 'false', 'null'), '', ini_get('display_errors'))) { $_error->show_php_error($severity, $message, $filepath, $line); } //下面两行只是根据配置文件判断要不要log错误信息而已。 if ($is_error) { exit(1); // EXIT_ERROR } } } //异常处理程序 if (!function_exists('_exception_handler')) { //发送给记录器未捕获的异常并显示只有display_errors是使他们不出现在生产环境中。 function _exception_handler($exception) { $_error =& load_class('Exceptions', 'core'); $_error->log_exception('error', 'Exception: ' . $exception->getMessage(), $exception->getFile(), $exception->getLine()); if (str_ireplace(array('off', 'none', 'no', 'false', 'null'), '', ini_get('display_errors'))) { $_error->show_exception($exception); } exit(1); } } //关机程序 if (!function_exists('_shutdown_handler')) { //这是停机处理,在codeigniter.php顶部声明。 //使用这个的主要原因是模拟一个完整的自定义异常处理程序。 function _shutdown_handler() { $last_error = error_get_last(); if (isset($last_error) && ($last_error['type'] & (E_ERROR | E_PARSE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING)) ) { _error_handler($last_error['type'], $last_error['message'], $last_error['file'], $last_error['line']); } } } //删除不可见字符 if (!function_exists('remove_invisible_characters')) { //这可以防止夹空字符之间的ASCII字符,如java \ 0script。 function remove_invisible_characters($str, $url_encoded = TRUE) { $non_displayables = array(); if ($url_encoded) { $non_displayables[] = '/%0[0-8bcef]/i'; // url encoded 00-08, 11, 12, 14, 15 $non_displayables[] = '/%1[0-9a-f]/i'; // url encoded 16-31 } $non_displayables[] = '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S'; // 00-08, 11, 12, 14-31, 127 do { $str = preg_replace($non_displayables, '', $str, -1, $count); } while ($count); return $str; } } //返回HTML转义变量 if (!function_exists('html_escape')) { function html_escape($var, $double_encode = TRUE) { if (empty($var)) { return $var; } if (is_array($var)) { foreach (array_keys($var) as $key) { $var[$key] = html_escape($var[$key], $double_encode); } return $var; } return htmlspecialchars($var, ENT_QUOTES, config_item('charset'), $double_encode); } } //stringify使用HTML标签属性 if (!function_exists('_stringify_attributes')) { //用于将字符串、数组或属性的对象转换为字符串的辅助函数。 function _stringify_attributes($attributes, $js = FALSE) { $atts = NULL; if (empty($attributes)) { return $atts; } if (is_string($attributes)) { return ' ' . $attributes; } $attributes = (array)$attributes; foreach ($attributes as $key => $val) { $atts .= ($js) ? $key . '=' . $val . ',' : ' ' . $key . '="' . $val . '"'; } return rtrim($atts, ','); } } //功能可用 if (!function_exists('function_usable')) { //执行function_exists()检查,如果了Suhosin PHP扩展加载-检查功能,检查可能有禁用为好。 function function_usable($function_name) { static $_suhosin_func_blacklist; if (function_exists($function_name)) { if (!isset($_suhosin_func_blacklist)) { $_suhosin_func_blacklist = extension_loaded('suhosin') ? explode(',', trim(ini_get('suhosin.executor.func.blacklist'))) : array(); } return !in_array($function_name, $_suhosin_func_blacklist, TRUE); } return FALSE; } }