Thinkphp源码分析系列(二)–引导类
在上一章我们说到,ThinkPHP.php在设置完框架所需要的变量和调教好环境后,在最后调用了 Think\Think::start(); 即Think命名空间中的Think类的静态方法start,从而开启了引用的初始化。Think\Think就是tp框架的引导类,这个类的主要作用就是负责诸多配置加载,注册核心系统扩展(自动加载类库、异常处理、错误处理等),管理和维护类实例、别名映射,可以一说是一个框架的工厂。
下面我们就来分析一下这个类。
1:类所在的路径
ThinkPHP\Library\Think\Think.class.php
2:类结构
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记录 }
3:源码分析
// +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK IT ] // +---------------------------------------------------------------------- // | Copyright (c) 2006-2014 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- // | Author: liu21st <liu21st@gmail.com> // +---------------------------------------------------------------------- namespace Think; /** * ThinkPHP 引导类 */ class Think { // 类映射 private static $_map = array(); // 实例化对象 private static $_instance = array(); /** * 应用程序初始化 * @access public * @return void 此方法是tp框架的核心方法,该方法包含一套错误和异常处理机制,作为ThinkPHP框架的引导接口,实现错误、异常处理,配置加载,别名映射,行为注册,包含运行缓存的生成,网站应用目录检测,自动类库加载行为注册。下面我们通过代码一一来说明tp是怎样实现上述功能的。 */ static public function start() { // 注册AUTOLOAD方法 /* 在此,tp利用php5的扩展函数spl_autoload_register来实现自己的自动加载逻辑。__autoload函数是php5的一个自动加载函数,当我们试图使用一个没有定义的类的时候,php内核会自动调用此函数。所以我们可以在此函数中写入代码让php自动加载类。而不必每次都引入很多类。一般来说此函数足以应付大多数场景,但是也有一些场景是这样的,我们很多类不在一个路径下,分布在很多路径下,那么如果都把查找逻辑写在autoload函数中的话会显得非常臃肿,所以就有了spl_autoload_register函数,此函数维护一个函数列表,我们可以调用此函数把我们已经写好的自动加载函数添加到函数列表中,那么当系统找不到类的时候,就会依次调用函数列表中的函数来实现查找类的效果。 tp在此使用了这个函数来注册了一个自己写的自动加载函数。Think\Think::autoload。其实直接使用__autoload也是可以的。 */ spl_autoload_register('Think\Think::autoload'); // 设定错误和异常处理 /* register-shutdown_function函数可以说是一个析构函数,也可以说是一个钩子函数,他的运行时机是php脚本结束的时候。 当我们的脚本执行完成或意外死掉导致PHP执行即将关闭时,我们的这个函数将会 被调用. register_shutdown_function 执行机制是:PHP把要调用的函数调入内存。当页面所有PHP语句都执行完成时,再调用此 函数。注意,在这个时候从内存中调用,不是从PHP页面中调用。 注意:register_shutdown_function是指在执行完所有PHP语句后再调用函数,不要理解成客户端关闭流浏览器页面时调用函数。 可以这样理解调用条件: 1、当页面被用户强制停止时 2、当程序代码运行超时时 3、当PHP代码执行完成时 */ register_shutdown_function('Think\Think::fatalError'); /* 下面是tp框架使用set_error_handler函数自定义的错误处理函数。如果使用了该函数,会完全绕过标准的 PHP 错误处理函数,如果必要,用户定义的错误处理程序必须终止 (die() ) 脚本。 */ set_error_handler('Think\Think::appError'); /* set_exception_handler就是设置当你的程序需要抛出一个异常的时候调用哪个自定义的函数。 php中抛异常使用Exception类,使用方法:throw new Execption("this is a exception"); 那么当执行这个语句的时候会自动调用我们转进去的自定义函数。 */ set_exception_handler('Think\Think::appException'); // 初始化文件存储方式 /* 这是tp的文件操作封装类。可以说是一个工厂模式吧。一般应用我们封装一个文件操作类就足以应付需求了。但是TP作为一个框架,需要考虑的方面很多。就拿这个文件操作来说,不同的环境实现方式不一样。tp就做了Storage这个类,我们可以在connect方法中传入一个类型参数,在内部,Storage根据参数不同实例化不同的文件操作类并保存其实例。在Think\Storage\Driver下面tp实现了file本地文件操作和sae环境下的文件操作。file类和sae实现类的方法名称都相同,类比于java中可以说是实现了同一个文件接口。这样的好处是我们只需要Storage->hander实例就可以用同一套接口来操作了。 默认的STORAGE_TYPE是file */ Storage::connect(STORAGE_TYPE); //根据当前的环境参数拼出运行时文件目录 $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 = ''; // 构建步骤1:根据当前环境的应用模式,读取应用模式配置文件 $mode = include is_file(CONF_PATH.'core.php')?CONF_PATH.'core.php':MODE_PATH.APP_MODE.'.php'; // 构建步骤2:读取配置文件中的core项,加载核心文件 foreach ($mode['core'] as $file){ if(is_file($file)) { include $file; if(!APP_DEBUG) $content .= compile($file); } } // 构建步骤3:加载应用模式配置文件 foreach ($mode['config'] as $key=>$file){ is_numeric($key)?C(load_config($file)):C($key,load_config($file)); } // 构建步骤4:读取当前应用模式对应的配置文件 if('common' != APP_MODE && is_file(CONF_PATH.'config_'.APP_MODE.CONF_EXT)) C(load_config(CONF_PATH.'config_'.APP_MODE.CONF_EXT)); // 构建步骤5:加载模式别名定义 if(isset($mode['alias'])){ self::addMap(is_array($mode['alias'])?$mode['alias']:include $mode['alias']); } // 构建步骤6:加载应用别名定义文件 if(is_file(CONF_PATH.'alias.php')) self::addMap(include CONF_PATH.'alias.php'); // 构建步骤7:加载模式行为定义 if(isset($mode['tags'])) { Hook::import(is_array($mode['tags'])?$mode['tags']:include $mode['tags']); } // 构建步骤8:加载应用行为定义 if(is_file(CONF_PATH.'tags.php')) // 允许应用增加开发模式配置定义 Hook::import(include CONF_PATH.'tags.php'); // 构建步骤9:加载框架底层语言包 L(include THINK_PATH.'Lang/'.strtolower(C('DEFAULT_LANG')).'.php'); /* 如果当前不是处于调试模式,创建运行时文件并写入编译好的代码。 这里使用php5.3以后的命名空间特性。参考http://developer.51cto.com/art/200907/137746.htm */ if(!APP_DEBUG){ //namespace {}这种方式用于声明代码块中的命名空间属于全局命名空间,这句代码用于生成加载别名映射的php代码 $content .= "\nnamespace { Think\Think::addMap(".var_export(self::$_map,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'.CONF_EXT)) C(include CONF_PATH.'debug'.CONF_EXT); } } // 读取当前应用状态对应的配置文件.根据APP_STATUS读取当前部署环境配置文件,常用在上线前数据库连接配置等,用于覆盖默认配置行为 if(APP_STATUS && is_file(CONF_PATH.APP_STATUS.CONF_EXT)) C(include CONF_PATH.APP_STATUS.CONF_EXT); // 设置系统时区 date_default_timezone_set(C('DEFAULT_TIMEZONE')); // 检查应用目录结构 如果不存在则自动创建 if(C('CHECK_APP_DIR')) { $module = defined('BIND_MODULE') ? BIND_MODULE : C('DEFAULT_MODULE'); if(!is_dir(APP_PATH.$module) || !is_dir(LOG_PATH)){ // 检测应用目录结构 Build::checkDir($module); } } // 记录加载文件时间 G('loadTime'); // 运行应用 App::run(); } // 注册classmap /* addMap和getMap以及$_map主要用来实现tp官方所说的类映射功能。主要用来解决命名空间较多带来的效率问题。我们可以通过此函数把常用的命名空间和类文件实际路径进行映射,这样php就不用再去寻找路径了,提高了运行效率。$_map是一个数组,键为命名空间字符串,值为对应的路径信息。 addMap有两种使用方法,如果我们只是添加一个映射关系,那么Think\Think::addMap('Think\Log',THINK_PATH.'Think\Log.php'); 如果我们要批量添加,第二个参数就不需要了。我们可以直接添加一个映射数组。从下面的实现代码中我们就可以看到,如果$class是一个数组的话,系统会直接和$_map数组进行合并。如果不是一个数组,就把$class和$map分别当做$_map数组的键和值。 */ static public function addMap($class, $map=''){ if(is_array($class)){ self::$_map = array_merge(self::$_map, $class); }else{ self::$_map[$class] = $map; } } // 获取classmap /* 根据键从$_map数组中取得对应的路径信息 */ static public function getMap($class=''){ if(''===$class){ return self::$_map; }elseif(isset(self::$_map[$class])){ return self::$_map[$class]; }else{ return null; } } /** * 类库自动加载 * @param string $class 对象类名 * @return void */ public static function autoload($class) { // 优先级1:检查是否存在映射,如果有的话就加载对应的文件路径 if(isset(self::$_map[$class])) { include self::$_map[$class]; } //优先级2:检查命名空间,如果存在\,就取\之前的字符串$name,如果$name是Library文件夹下的任何一个子文件夹,就可以定位$oath为当前的类库路径。即Library。 elseif(false !== strpos($class,'\\')){ $name = strstr($class, '\\', true); if(in_array($name,array('Think','Org','Behavior','Com','Vendor')) || is_dir(LIB_PATH.$name)){ // Library目录下面的命名空间自动定位 $path = LIB_PATH; }else{ //优先级3: 检测自定义命名空间 否则就以模块为命名空间 /* 一般来说我们的类都会写在Library文件夹下,所以上面的代码会先从Library文件夹下去搜索。 但是我们也可以不写在Library文件夹下。我们可以自己在Thinkphp文件夹下创建一个和Library平级的文件夹,tp把他叫做创建了一个新的命名空间。 当我们检测到存在此文件夹,就返回其所在的目录。如果不存在, 我们就进入优先级4:$path = ARR_PATH. 则会当作模块的命名空间进行自动加载 */ $namespace = C('AUTOLOAD_NAMESPACE'); $path = isset($namespace[$name])? dirname($namespace[$name]).'/' : APP_PATH; } /* 经过上面的四个顺序后,我们就会得到$path 接下来就拿$path和命名空间路径来拼凑真正对应的文件路径。 */ $filename = $path . str_replace('\\', '/', $class) . EXT; /* 检测如果$filename是文件,win环境下区分大小写 */ if(is_file($filename)) { // Win环境下面严格区分大小写 if (IS_WIN && false === strpos(str_replace('/', '\\', realpath($filename)), $class . EXT)){ return ; } include $filename; } }elseif (!C('APP_USE_NAMESPACE')) { // 自动加载的类库层 /* tp在配置文件中可以通过APP_USE_NAMESPACE来定义是否使用5.3的新特性命名空间。 如果你配置为false,那么tp就需要你再配置一个APP_AUTOLOAD_LAYER或者 APP_AUTOLOAD_PATH来告诉tp应该去哪个文件夹下去寻找你要使用的类。 */ foreach(explode(',',C('APP_AUTOLOAD_LAYER')) as $layer){ if(substr($class,-strlen($layer))==$layer){ if(require_cache(MODULE_PATH.$layer.'/'.$class.EXT)) { return ; } } } // 根据自动加载路径设置进行尝试搜索 /*这里就是在没有使用命名空间的情况下使用APP_AUTOLOAD_PATH来定义要加载类的路径*/ foreach (explode(',',C('APP_AUTOLOAD_PATH')) as $path){ if(import($path.'.'.$class)) // 如果加载类成功则返回 return ; } } } /** * 取得对象实例 支持调用类的静态方法 * @param string $class 对象类名 * @param string $method 类的静态方法名 * @return object 此方法的作用就是你传进去一个类,他给你返回一个该类的实例,保存在$_instance数组中,数组键为$identify,值为对应的实例。此外,此方法还支持调用类的静态方法,如果你除了传入类名外,还传入了一个该类所有的方法名,那么instance方法返回的就不会类所对应的实例了,而是返回执行了类的静态方法后,该静态方法返回的值。 */ static public function instance($class,$method='') { //类的唯一标识,用类名和方法名组成 $identify = $class.$method; //检测$_instance变量中是否存在改标识,如果不存在,那么再检查类是否存在,如果存在就实例化。在类存在的前提下去检查方法是否存在,如果方法也存在就调用并返回方法的执行结果给$_instance.如果方法不存在,就直接返回类的实例给$_instace. 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]; } /** * 自定义异常处理 * @access public * @param mixed $e 异常对象 改函数由set_exception_handler调用 */ static public function appException($e) { $error = array(); //获得异常错误信息 $error['message'] = $e->getMessage(); /*得到异常的错误信息数组字符串 一般的格式如下: array(1) { [0]=> array(6) { ["file"]=> string(54) "/....../test.php" ["line"]=> int(37) ["function"]=> string(11) "__construct" ["class"]=> string(4) "Test" ["type"]=> string(2) "->" ["args"]=> array(0) { } } } */ $trace = $e->getTrace(); /* tp的公共函数库中有一个函数E,专门给开发者使用。用于让开发者抛出异常。所以这里首先检查第一个函数是不是E,如果是的话返回错误文件和错误行数 */ 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: /* 清空输出缓冲, 其实就是把php默认的错误输出给清除掉 */ ob_end_clean(); $errorStr = "$errstr ".$errfile." 第 $errline 行."; /* 根据LOG_RECORD是否记录错误信息,决定是否写入错误日志 */ if(C('LOG_RECORD')) Log::write("[$errno] ".$errorStr,Log::ERR); self::halt($errorStr); break; default: $errorStr = "[$errno] $errstr ".$errfile." 第 $errline 行."; self::trace($errorStr,'','NOTIC'); break; } } // 致命错误捕获 static public function fatalError() { Log::save(); 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){ 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 { $message = is_array($error) ? $error['message'] : $error; $e['message'] = C('SHOW_ERROR_MSG')? $message : C('ERROR_MESSAGE'); } } // 包含异常页面模板 $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) { //采用静态变量存储错误信息 static $_trace = array(); if('[think]' === $value){ // 获取trace信息 return $_trace; }else{ $info = ($label?$label.':':'').print_r($value,true); //将错误级别转为大写 $level = strtoupper($level); //如果是ajax请求那么就不显示trace调试工具或者就记录到日志中区 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')) { $_trace[$level] = array(); } $_trace[$level][] = $info; } } } }
个人微信:879008785 qq:879008785 欢迎学习交流;