CI框架源码解析三之全局函数库文件Common.php

        从本篇开始,基本上算是深入到了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;
        }
    }

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值