ThinkPHP框架引导类分析
该类文件在:ThinkPHP/Library/Think/Think.class.php
该类可以说是ThinkPHP框架最为核心的类库,负责诸多配置加载,注册核心系统扩展(自动加载类库、异常处理、错误处理等),管理和维护类实例、别名映射,可以一说是一个框架的工厂(该类有些许面向对象弊端,比如:违背了面向对象单一职责,其负责功能复杂,关联类库和文件较多,有动一牵百的忧虑)。类中遇到的函数会在该类分析之后彻底分析,所涉及的其它类库会专门讲解。
一、类结构
namespace Think;//定义命名空间class Think { private static $_map = array();//类库别名映射 private static $_instance = array();//保存类实例(这么说也不合理,等会分析该功能时具体说明) static public function start() {}//应用程序初始化 static public function addMap($class, $map=''){}// 注册classmap static public function getMap($class=''){}// 获取classmap public static function autoload($class) {}//类库自动加载 static public function instance($class,$method='') {}//取得对象实例 支持调用类的静态方法 static public function appException($e) {}//自定义异常处理 static public function appError($errno, $errstr, $errfile, $errline) {}//自定义错误处理 static public function fatalError() {} // 致命错误捕获 static public function halt($error) {}//错误输出 static public function trace($value='[think]',$label='',$level='DEBUG',$record=false) {}//添加和获取页面Trace记录}
二、应用程序初始化start()方法分析,该方法包含一套错误和异常处理机制,非常受用。该方法作为ThinkPHP框架的引导接口,实现错误、异常处理,配置加载,别名映射,行为注册,包含运行缓存的生成,网站应用目录检测,自动类库加载行为注册。
/** * 应用程序初始化 * @access public * @return void */ static public function start() { //使用spl标准库中提供__autoload()函数的默认实现,比__autoload()效率更高,更加灵活 //一下可以使用spl_autoload_register(array('Think\Think','autoload')); //建议使用spl_autoload_register(__NAMESPACE__.'\Think::autoload');实现 //一下所有注册方式均可以使用上面3中形式传递参数 spl_autoload_register('Think\Think::autoload'); //注册全局脚本"析构函数",使用该方式注册的函数,会在脚本结束前调用,大多数情况用来处理致命错误 register_shutdown_function('Think\Think::fatalError'); //设置自定义错误处理函数,用于处理错误信息 set_error_handler('Think\Think::appError'); //设置未异常处理函数 set_exception_handler('Think\Think::appException'); //可以把register_shutdown_function(),set_error_handler(),set_error_handler()3个函数组合完成自定义、多元化的错误处理模块 //根据STORAGE_TYPE的值设置分布式文件存储方案,Storage是一个工厂类,用于管理和维护分布式文件存储组件 //后面会详细讲解Storage类,并指出设计缺陷 Storage::connect(STORAGE_TYPE); //根据运行模式在运行缓存目录下生成编译缓存文件APP_MODE.'~runtime.php',从而减少IO开销 //下面会详细介绍生成缓存文件的方式 $runtimefile = RUNTIME_PATH.APP_MODE.'~runtime.php'; //如果不是在调试模式,并且编译缓存文件存在,直接加载编译缓存 if(!APP_DEBUG && Storage::has($runtimefile)){ Storage::load($runtimefile); }else{ //判断编译缓存文件是否存在,存在就删除 if(Storage::has($runtimefile)) Storage::unlink($runtimefile); //预编译内容变量 $content = ''; //判断是否存在运行模式配置文件,如果不存在就加载MODE_PATH.APP_MODE.'.php',运行模式配置文件会影响下列加载不同的类库和配置 //运行配置文件后期会详细讲解 $mode = include is_file(CONF_PATH.'core.php')?CONF_PATH.'core.php':MODE_PATH.APP_MODE.'.php'; //以下所有配置项加载都会根据加载的先后顺序覆盖之前的配置项,一般都是先加载ThinkPHP默认配置,再加载应用配置 //core下标决定要加载的核心类和函数文件 foreach ($mode['core'] as $file){ if(is_file($file)) { include $file; //如果不是调试模式,则编译该文件内容并储存到预编译内容变量中 if(!APP_DEBUG) $content .= compile($file); } } //config下标决定要加载的核心配置文件 foreach ($mode['config'] as $key=>$file){ //判断下标是否为数字,如果不是就会把该配置文件中的配置项加载到对应的键下面,相当于给配置项增加一个纬度 is_numeric($key)?C(include $file):C($key,include $file); } //如果不是普通运行模式,则判断是否存在运行模式应用配置文件 if('common' != APP_MODE && is_file(CONF_PATH.'config_'.APP_MODE.'.php')) C(include CONF_PATH.'config_'.APP_MODE.'.php'); //alias下标记录类库别名映射规则,ThinkPHP独创别名机制,用于提升自动加载的效率 if(isset($mode['alias'])){ //由这句代码可以看出alias规则可以是一个数组,或者将规则数组单独作为一个文件 self::addMap(is_array($mode['alias'])?$mode['alias']:include $mode['alias']); } //加载应用中定义的别名配置 if(is_file(CONF_PATH.'alias.php')) self::addMap(include CONF_PATH.'alias.php'); //tags下标用于标识系统行为,行为扩展具体由Hook钩子类实现 if(isset($mode['tags'])) { //由这句代码可以看出tags规则可以是一个数组,或者将规则数组单独作为一个文件 Hook::import(is_array($mode['tags'])?$mode['tags']:include $mode['tags']); } //加载应用中的行为扩展配置 if(is_file(CONF_PATH.'tags.php')) // 允许应用增加开发模式配置定义 Hook::import(include CONF_PATH.'tags.php'); //加载框架底层语言包,有核心配置文件中的DEFAULT_LANG配置项决定 L(include THINK_PATH.'Lang/'.strtolower(C('DEFAULT_LANG')).'.php'); //如果不是调试模式则生成编译缓存文件 if(!APP_DEBUG){ //namespace {}这种方式用于声明代码块中的命名空间属于全局命名空间 //这句代码用于生成加载别名映射的php代码 $content .= "\nnamespace { Think\Think::addMap(".var_export(self::$_map,true).");"; //L(".var_export(L(),true).");生成语言加载代码 //C(".var_export(C(),true).');生成配置项加载代码 //Think\Hook::import('.var_export(Hook::get(),true).');生成钩子加载代码 $content .= "\nL(".var_export(L(),true).");\nC(".var_export(C(),true).');Think\Hook::import('.var_export(Hook::get(),true).');}'; //将$content变量内容去除注释和换行、空隔之后写入到运行时编译缓存文件 Storage::put($runtimefile,strip_whitespace('<?php '.$content)); }else{ // 调试模式加载系统默认的配置文件 C(include THINK_PATH.'Conf/debug.php'); // 读取应用调试配置文件 if(is_file(CONF_PATH.'debug.php')) C(include CONF_PATH.'debug.php'); } } //根据APP_STATUS读取当前部署环境配置文件,常用在上线前数据库连接配置等,用于覆盖默认配置行为 if(APP_STATUS && is_file(CONF_PATH.APP_STATUS.'.php')) C(include CONF_PATH.APP_STATUS.'.php'); // 设置系统时区 //建议可以写成date_default_timezone_set(C('DEFAULT_TIMEZONE',null,'PRC'));防止配置项读取问题导致的时区设置错误 date_default_timezone_set(C('DEFAULT_TIMEZONE')); // 检查应用目录结构 如果不存在则自动创建 if(C('CHECK_APP_DIR') && !is_dir(LOG_PATH)) { //build.php负责创建应用目录结构 require THINK_PATH.'Common/build.php'; } // 记录加载文件时间 G('loadTime'); // 运行应用 App::run(); }
三、类库别名映射机制实现addMap()和getMap()方法分析;该机制使用Think::_map变量存储别名映射记录,通过Think::addMap()添加或修改别名映射记录,使用Think:getMap()获取别名映射记录。
/** * 注册或修改类库别名映射记录 * @access public * @param class String|Array 如果为数组键为类库别名,键值为类库实际位置;如果字符串代表类库别名 * @param map String 如果class为字符串,该参数代表类库实际位置,否则没有意义 * @return void */ static public function addMap($class, $map=''){ //判断class是否为数组,如果是就合并当前类库别名记录,如果有相同记录就会覆盖 if(is_array($class)){ self::$_map = array_merge(self::$_map, $class); }else{ //如果class不是数组,就作为别名储存在类库别名记录中,并把map作为实际类库位置 self::$_map[$class] = $map; } } /** * 根据别名获取类库实际地址 * @param class String 类库别名 * @return Array|String|NULL 如果class为空则获取所有类库别名记录,否者返回别名对应的类库位置 */ static public function getMap($class=''){ //如果class为空,直接返回所有类库别名记录 if(''===$class){ return self::$_map; //判断对应别名是否在别名映射记录中,如果存在返回类库实际地址,否者返回null }elseif(isset(self::$_map[$class])){ return self::$_map[$class]; }else{ return null; } }
四、ThinkPHP类库自动加载机制autoload()方法分析,该方法是由Think::start()方法中的第一句代码注册实现spl_autoload_register('Think\Think::autoload');
/** * 类库自动加载 * @param string $class 对象类名 * @return void */ public static function autoload($class) { //判断是否存在别名映射 //标记一处bug,如果使用Think::addMap('Think\Test');注册别名就完了,程序逻辑不严谨,不会有大的安全问题,可以无视 //具体可以看Think::addMap()方法的实现 if(isset(self::$_map[$class])) { include self::$_map[$class]; //建议在这里renturn; //判断是否存在\符号,存在则使用命名空间加载机制 //此处与配置说明不符'APP_AUTOLOAD_PATH' => '', // 自动加载的路径 关闭APP_USE_NAMESPACE后有效 //现在判断的是否在类名中使用命名规则而不是使用APP_USE_NAMESPACE配置,当然该项配置主要作用在路由模块,后续会讲解 }elseif(strpos($class,'\\')){ //获取命名空间的第一个命名范围 $name = strstr($class, '\\', true); //判断该命名空间的第一个命名范围是否在Think约定范围类,并在Library目录下存在该目录 if(in_array($name,array('Think','Org','Behavior','Com','Vendor')) || is_dir(LIB_PATH.$name)){ // Library目录下面的命名空间自动定位 $path = LIB_PATH; }else{ //检测自定义命名空间 否则就以模块为命名空间 $namespace = C('AUTOLOAD_NAMESPACE'); $path = isset($namespace[$name])? dirname($namespace[$name]).'/' : APP_PATH; } //这里可以看出ThinkPHP命名空间的命名规则是以LIB_PATH和APP_PATH作为根目录的目录原则,也可以为AUTOLOAD_NAMESPACE配置项来自定义命名规则根目录 $filename = $path . str_replace('\\', '/', $class) . EXT; //判断文件是否存在,为啥在注册别名的时候不去判断文件是否存在呢?那样是否可以无视一些有问题的别名映射,而使用正确的加载规则呢? if(is_file($filename)) { //如果在windows环境下运行,使用strpos来检测是否大小写一致问题,如果不一致直接返回 if (IS_WIN && false === strpos(str_replace('/', '\\', realpath($filename)), $class . EXT)){ return ; } //导入类文件 include $filename; //这里建议return; } }else{ //不用按照配置文件中的APP_USE_NAMESPACE的值来决定是否APP_AUTOLOAD_LAYER配置该项 //只要在类库加载中没有使用命名空间就会调用以下规则来查找类库(要实例化的类不能声明命名规则) foreach(explode(',',C('APP_AUTOLOAD_LAYER')) as $layer){ //判断类名最后几位是否符合APP_AUTOLOAD_LAYER配置项 if(substr($class,-strlen($layer))==$layer){ //加载当前模块下对应的类文件,这个其实可以直接判断文件是否存在,并用include加载即可,没有必要调用require_cache函数 //以上是个人见解,缘由是因为上面的加载机制都没有使用,我觉得是否应该统一,而且自动加载类文件,不用限制加载一次,如果已经加载了,也不会调用这个方法了。 if(require_cache(MODULE_PATH.$layer.'/'.$class.EXT)) { //加载文件成功直接返回 return ; } } } //根据APP_AUTOLOAD_PATH配置设置的路径规则自动搜索并加载文件 foreach (explode(',',C('APP_AUTOLOAD_PATH')) as $path){ //这里同上,是否也可以不用调用import()方法加载,或者统一了呢? if(import($path.'.'.$class)) // 如果加载类成功则返回 return ; } } }
五、管理类实例或者'缓存'类静态方法调用结果instance()方法分析,之前在类结构分析中说$_instance变量保存类实例并不合理,因为该类还可以调用类的静态方法,并'缓存'结果。我在该方法注释中提出一些个人见解,并不是说该方法设计的不合理,这个是毕竟是ThinkPHP专有方法,任何一个项目的设计都不会像我那般去考虑一个方法的诸多问题,首要问题是解决是否符合项目应用足矣。同样看到这篇文章的朋友可以思考,在项目是否有很多类并不需要多个实例(没有强制约束的情况下),如果有那可以设计一个适合自己项目的伪单例工厂来管理这些类的实例。
/** * 取得对象实例 支持调用类的静态方法 * 解析:我把该类看成一个伪单例工厂,不是严格要求类不许是单例,统一使用该方法获取类对象,可以实现单例模式(极其适合php这种较为灵活的语言) * 说这是一个单例模式工厂不合格,因为没有严格要求所管理类必须符合单例模式约束。 * 问题:该方法说此类可以调用类的静态方法,并没有约定静态方法必须返回类的实例(self),可以返回任意结果,这个让我很诧异 * 如果是想要缓存类方法调用结果,是否应该提供给方法传递参数选项呢? * 如果仅仅为了管控类的实例(比如严格按照单例模式设计的类,如果要管理,必须使用静态方法),是否应该说明或者在方法中检测返回值呢? * 解惑:这个毕竟不是提供给应用的方法(当然可以使用,设计初衷肯定不是,这算是ThinkPHP开发团队约定俗成的规定(口头约束使用方法)吧?) * 这至少给我们一个启示,可以这么管理伪单例模式(口头约束的方式,当然比之更可靠),之前我在一个项目中设计过这样一个工厂类(那时候还没有分析过任何产品的源码) * @param string $class 对象类名 * @param string $method 类的静态方法名 * @return object */ static public function instance($class,$method='') { //生成实例管理标识 $identify = $class.$method; //判断是否已经存在改类实例标识 if(!isset(self::$_instance[$identify])) { //判断类是否存在,这里反应不能使用自动加载机制,必须在调用该方法前加载类文件 if(class_exists($class)){ //实例化类,这里可以看出并不能管理严格意义上的单例类 $o = new $class(); //判断是否要调用静态方法,并确定该类是否存在该方法 if(!empty($method) && method_exists($o,$method)) //返回调用结果(不明确是什么) self::$_instance[$identify] = call_user_func(array(&$o, $method)); else //存储类实例对象 self::$_instance[$identify] = $o; } else //输出错误信息 self::halt(L('_CLASS_NOT_EXIST_').':'.$class); } //返回实例对象 return self::$_instance[$identify]; }
六、ThinkPHP内置错误处理和异常处理实现分析。appException()方法由Think::start()中set_exception_handler('Think\Think::appException');语句实现。appError()方法由Think::start()中set_error_handler('Think\Think::appError');语句实现,fatalError()方法由Think::start()中register_shutdown_function('Think\Think::fatalError');语句实现。halt()方法用来输出重要错误信息和异常,并终止程序执行。trace()方法用来记录并管理Trace调试工具中的错误信息。
/** * 自定义异常处理 * @access public * @param mixed $e 异常对象 * @param void */ static public function appException($e) { $error = array(); //获取异常错误信息 $error['message'] = $e->getMessage(); //获取backtrace()回溯信息 $trace = $e->getTrace(); //判断是否由异常处理方法抛出,ThinkPHP自定义抛出异常处理函数 if('E'==$trace[0]['function']) { $error['file'] = $trace[0]['file'];//获取错误文件 $error['line'] = $trace[0]['line'];//获取错误行号 }else{ $error['file'] = $e->getFile();//获取错误文件 $error['line'] = $e->getLine();//获取错误行号 } //已格式错误回溯信息 $error['trace'] = $e->getTraceAsString(); //写入到错误日志 Log::record($error['message'],Log::ERR); // 发送404信息 header('HTTP/1.1 404 Not Found'); header('Status:404 Not Found'); //显示错误信息 self::halt($error); } /** * 自定义错误处理 * @access public * @param int $errno 错误类型 * @param string $errstr 错误信息 * @param string $errfile 错误文件 * @param int $errline 错误行数 * @return void */ static public function appError($errno, $errstr, $errfile, $errline) { switch ($errno) { //一些重要的错误信息,会影响之后的程序执行 case E_ERROR: case E_PARSE: case E_CORE_ERROR: case E_COMPILE_ERROR: case E_USER_ERROR: //清空输出缓冲(不知道在哪里开启了,后面分析会遇到) ob_end_clean();//其实就是把php默认输出的错误信息清除掉 //错误信息 $errorStr = "$errstr ".$errfile." 第 $errline 行."; //根据LOG_RECORD是否记录错误信息,决定是否写入错误日志 if(C('LOG_RECORD')) Log::write("[$errno] ".$errorStr,Log::ERR); //输出错误信息 self::halt($errorStr); break; //可以忽略的错误信息,不会输出,会记录到trace当中,使用SHOW_PAGE_TRACE配置可以查看的错误信息 default: //这里程序还要继续执行,不能清空输出缓冲,如果不希望显示这类错误信息,应当在php.ini中调节,或者使用ini_set()的函数改变 //错误信息 $errorStr = "[$errno] $errstr ".$errfile." 第 $errline 行."; //记录到trace当中 self::trace($errorStr,'','NOTIC'); break; } } // 致命错误捕获 static public function fatalError() { //致命错误必须保存到日志中 Log::save(); //获取上一个错误信息,没有错误就跳过了(⊙0⊙) if ($e = error_get_last()) { //处理致命错误信息 switch($e['type']){ case E_ERROR: case E_PARSE: case E_CORE_ERROR: case E_COMPILE_ERROR: case E_USER_ERROR: //清空输出缓存,都导致程序停止了,还显示什么呀 ob_end_clean(); //输出错误信息 self::halt($e); break; } } } /** * 错误输出 * @param mixed $error 错误 * @return void */ static public function halt($error) { $e = array(); //判断是否是调试模式,或者命令行模式 if (APP_DEBUG || IS_CLI) { //调试模式下输出错误信息 //如果错误信息不是一个数组,就回溯最后一次执行方法的信息 if (!is_array($error)) { $trace = debug_backtrace(); $e['message'] = $error; $e['file'] = $trace[0]['file']; $e['line'] = $trace[0]['line']; ob_start();//开始输出缓存 debug_print_backtrace();//输出一条回溯信息 $e['trace'] = ob_get_clean();//获取输出缓冲信息,并清空 } else { $e = $error; } if(IS_CLI){ //命令行模式,转换为gbk编码,终止程序执行并输出错误信息 exit(iconv('UTF-8','gbk',$e['message']).PHP_EOL.'FILE: '.$e['file'].'('.$e['line'].')'.PHP_EOL.$e['trace']); } } else { //不是调试模式,重定向到错误页面 $error_page = C('ERROR_PAGE');//获取设置的错误页面 if (!empty($error_page)) { //重定向到错误页面 redirect($error_page); } else { //根据SHOW_ERROR_MSG配置决定是否显示详细的错误信息,还是采用ERROR_MESSAGE设定的错误信息 $message = is_array($error) ? $error['message'] : $error; $e['message'] = C('SHOW_ERROR_MSG')? $message : C('ERROR_MESSAGE'); } } //根据TMPL_EXCEPTION_FILE配置决定调用错误信息显示模版,否者采用ThinkPHP默认模版 $exceptionFile = C('TMPL_EXCEPTION_FILE',null,THINK_PATH.'Tpl/think_exception.tpl'); include $exceptionFile; exit;//终止程序运行,很重要的。 } /** * 添加和获取页面Trace记录 * @param string $value 变量 * @param string $label 标签 * @param string $level 日志级别(或者页面Trace的选项卡) * @param boolean $record 是否记录日志 * @return void */ static public function trace($value='[think]',$label='',$level='DEBUG',$record=false) { //采用静态变量存储Trace记录 static $_trace = array(); if('[think]' === $value){ // 获取trace信息 return $_trace; }else{ //错误信息 $info = ($label?$label.':':'').print_r($value,true); $level = strtoupper($level);//将错误级别转换为大写 //如果是AjAX请求或者不显示TRACE调试工具,或者$record要求记录日志,就不会记录该条错误信息 if((defined('IS_AJAX') && IS_AJAX) || !C('SHOW_PAGE_TRACE') || $record) { Log::record($info,$level,$record);//将错误信息写入到日志文件 }else{ //判断错误等级是否存在或者该类错误信息是否达到错误类型记录上限,由TRACE_MAX_RECORD配置 if(!isset($_trace[$level]) || count($_trace[$level])>C('TRACE_MAX_RECORD')) { //这里有个我很诧异的地方,当错误类别记录达到错误类型记录上限的是否为什么要重置该类型错误记录 //而不是不记录当前错误信息,或者删除最先一条的错误信息,追加到最后 //我建议是不理会比较合理,因为调试错误,也有先后吗,先把之前遇到的错误解决,就会看到新的错误了(这是否有点坑⊙0⊙) $_trace[$level] = array(); } //按错类别记录错误信息 $_trace[$level][] = $info; } } }
七、总结:对该类分析,主要掌控php错误处理和异常处理方面的知识,并了解基于命名空间自动加载的规则定义基础,同样接触了ThinkPHP运行时编译缓存机制带来的IO优化思路以及类库别名机制对与类自动加载带来的优化。在分析该类时站在ThinkPHP应用外对该类提出几处质疑,仅为个人对面向对象设计的理解和认知,不作为详细参考。