TP框架源码分析 TP框架执行流程图 接下来几篇文章,通过xdebug的单步调试一步一步分析TP框架的源码,搞清楚他的框架实现,以及一些错误,异常,钩子等等的手法,看看我们的请求到底在TP内部产生了哪些变化
0x01 记录内存使用(4) 1 2 3 4 5 $GLOBALS['_beginTime' ] = microtime(TRUE ); define('MEMORY_LIMIT_ON' ,function_exists('memory_get_usage' )); if (MEMORY_LIMIT_ON) $GLOBALS['_startUseMems' ] = memory_get_usage();
microtime — 返回当前 Unix 时间戳和微秒数
第2,3行判断了memory_get_usage
方法是否存在,如果存在的话则调用memory_get_usage
方法得到系统为php分配的内存,并且在全局变量中注册,下图为当前各变量的值和状态
0x02 各种常量定义(5)
定义了各种不同的url模式,在配置文件中可以更改
定义了类文件的后缀名为.class.php
定义了一些路径常量
THINK_PATH: /usr/local/var/www/test/ThinkPHP/
TP框架各种核心文件的根目录
LIB_PATH: /usr/local/var/www/test/ThinkPHP/Library/
类库目录
APP_PATH: ./Application/
应用目录
COMMON_PATH: ./Application/Common/
应用公共目录
RUNTIME_PATH: ./Application/Runtime/
应用运行目录
判断是否为SAE环境,如果不是为普通模式,存储类型默认为FILE
定义各种核心类库,运行时目录等等在运行时需要的路径常量,都是与之前的路径常量拼接得到的
0x03 对运行环境的判断 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 if (version_compare(PHP_VERSION,'5.4.0' ,'<' )) { ini_set('magic_quotes_runtime' ,0 ); define('MAGIC_QUOTES_GPC' ,get_magic_quotes_gpc()? true : false ); }else { define('MAGIC_QUOTES_GPC' ,false ); } define('IS_CGI' ,(0 === strpos(PHP_SAPI,'cgi' ) || false !== strpos(PHP_SAPI,'fcgi' )) ? 1 : 0 ); define('IS_WIN' ,strstr(PHP_OS, 'WIN' ) ? 1 : 0 ); define('IS_CLI' ,PHP_SAPI=='cli' ? 1 : 0 ); if (!IS_CLI) { if (!defined('_PHP_FILE_' )) { if (IS_CGI) { $_temp = explode('.php' ,$_SERVER['PHP_SELF' ]); define('_PHP_FILE_' , rtrim(str_replace($_SERVER['HTTP_HOST' ],'' ,$_temp[0 ].'.php' ),'/' )); }else { define('_PHP_FILE_' , rtrim($_SERVER['SCRIPT_NAME' ],'/' )); } } if (!defined('__ROOT__' )) { $_root = rtrim(dirname(_PHP_FILE_),'/' ); define('__ROOT__' , (($_root=='/' || $_root=='\\' )?'' :$_root)); } } require CORE_PATH.'Think' .EXT;
通过PHP版本和函数判断GPC是否开启,在5.4.0版本以下可以通过get_magic_quotes_gpc()
来判断GPC是否开启,在5.4.0以后GPC被废除,所以直接定义MAGIC_QUOTES_GPC
为false
判断PHP的SAPI是否为CGI,是否为WIN或者CLI
如果不是CLI模式,并且没有定义_PHP_FILE_
的话,就定义_PHP_FILE_
的值为当前脚本的路径(即为index.php入口文件的路径)
定义__ROOT__
的值为tp框架所在的文件夹
最后包含核心框架类
以下为各种常量在本机的值(在不同的机器上有所不同):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 APP_DEBUG = true APP_PATH = "./Application/" MEMORY_LIMIT_ON = true THINK_VERSION = "3.2.3" URL_COMMON = 0 URL_PATHINFO = 1 URL_REWRITE = 2 URL_COMPAT = 3 EXT = ".class.php" THINK_PATH = "/usr/local/var/www/test/ThinkPHP/" APP_STATUS = "" APP_MODE = "common" STORAGE_TYPE = "File" RUNTIME_PATH = "./Application/Runtime/" LIB_PATH = "/usr/local/var/www/test/ThinkPHP/Library/" CORE_PATH = "/usr/local/var/www/test/ThinkPHP/Library/Think/" BEHAVIOR_PATH = "/usr/local/var/www/test/ThinkPHP/Library/Behavior/" MODE_PATH = "/usr/local/var/www/test/ThinkPHP/Mode/" VENDOR_PATH = "/usr/local/var/www/test/ThinkPHP/Library/Vendor/" COMMON_PATH = "./Application/Common/" CONF_PATH = "./Application/Common/Conf/" LANG_PATH = "./Application/Common/Lang/" HTML_PATH = "./Application/Html/" LOG_PATH = "./Application/Runtime/Logs/" TEMP_PATH = "./Application/Runtime/Temp/" DATA_PATH = "./Application/Runtime/Data/" CACHE_PATH = "./Application/Runtime/Cache/" CONF_EXT = ".php" CONF_PARSE = "" ADDON_PATH = "./Application/Addon" MAGIC_QUOTES_GPC = false
之后程序调用了Think.class.php的start()方法
0x04 实现错误与异常处理以及自动加载机制(7) 1 2 3 4 5 6 spl_autoload_register('Think\Think::autoload' ); register_shutdown_function('Think\Think::fatalError' ); set_error_handler('Think\Think::appError' ); set_exception_handler('Think\Think::appException' );
这里有几个函数,分别来看看他们的用法:
spl_autoload_register()
: 注册给定的函数作为 __autoload
的实现,而__autoload
会尝试加载未定义的类,实现了自动加载,以下是php.net给的官方说明
在注册自己的自动加载机制以后,TP实现了自己的致命错误处理,应用错误处理和应用异常处理机制,替换了php自己的错误处理机制
fatalError 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 ; } } }
调用Log::save()将错误记录到日志中
通过error_get_last
函数获得最后一次发生的错误
判断错误的类型
如果是E_USER_ERROR
的话通过ob_end_clean
输出缓冲区内容,并且将错误信息传入halt函数,此函数为TP定义的错误输出函数
appError 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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(); $errorStr = "$errstr " .$errfile." 第 $errline 行." ; 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 ; } }
和fatalErorr有些相像,先判断是那种类型的错误
如果是E_USER_ERROR
的话,输出并清除缓冲区内容
如果设置了LOG_RECORD,则在日志文件中记录这次错误
调用halt函数将错误输出
如果不是错误而逝Notice的话,通过trace函数输出该Notice
appException 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 static public function appException ($e) { $error = array (); $error['message' ] = $e->getMessage(); $trace = $e->getTrace(); 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); header('HTTP/1.1 404 Not Found' ); header('Status:404 Not Found' ); self ::halt($error); }
当发生异常并且没有处理的时候,调用appException
获取错误信息,错误路径,获取发生错误的方法,文件,行数
在日志文件中记录当前的错误信息
向用户发送404状态码,并且输出错误信息
0x05 存储初始化(8) 1 2 Storage::connect(STORAGE_TYPE);
在connect方法中,定义了一些文件的操作函数,如文件删除,追加,创建,读取等
0x06 缓存文件的定义与获取以及DEBUG模式的文件加载(9~22) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 $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 = include is_file(CONF_PATH.'core.php' )?CONF_PATH.'core.php' :MODE_PATH.APP_MODE.'.php' ; foreach ($mode['core' ] as $file){ if (is_file($file)) { include $file; if (!APP_DEBUG) $content .= compile($file); } } foreach ($mode['config' ] as $key=>$file){ is_numeric($key)?C(load_config($file)):C($key,load_config($file)); } if ('common' != APP_MODE && is_file(CONF_PATH.'config_' .APP_MODE.CONF_EXT)) C(load_config(CONF_PATH.'config_' .APP_MODE.CONF_EXT)); if (isset ($mode['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' ); if (isset ($mode['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' ); L(include THINK_PATH.'Lang/' .strtolower(C('DEFAULT_LANG' )).'.php' ); if (!APP_DEBUG){ $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 ).');}' ; 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); } } 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();
定义$runtimefile
的目录:./Application/Runtime/common~runtime.php
如果不是DEBUG模式并且存在缓存文件的话,直接加载缓存文件
如果是DEBUG模式,如果缓存文件存在的话则删除
读取当前应用模式,默认为普通模式,由APP_MODE定义,当前为普通模式
加载类型别名定义,实际上是将命名空间映射成对应文件的绝对路径
加载模式行为定义
加载底层语言包
加载调试模式的配置文件
判断应用状态并读取状态配置文件(如果APP_STATUS常量定义不为空的话)
调用App::run()运行应用
0x06 运行应用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static public function run () { Hook::listen('app_init' ); App::init(); Hook::listen('app_begin' ); if (!IS_CLI){ session(C('SESSION_OPTIONS' )); } G('initTime' ); App::exec(); Hook::listen('app_end' ); return ; }
应用初始化函数:App::init() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 static public function init () {load_ext_file(COMMON_PATH); C('LOG_PATH' , realpath(LOG_PATH).'/Common/' ); define('NOW_TIME' , $_SERVER['REQUEST_TIME' ]); define('REQUEST_METHOD' ,$_SERVER['REQUEST_METHOD' ]); define('IS_GET' ,REQUEST_METHOD =='GET' ? true : false ); define('IS_POST' , REQUEST_METHOD =='POST' ? true : false ); define('IS_PUT' ,REQUEST_METHOD =='PUT' ? true : false ); define('IS_DELETE' , REQUEST_METHOD =='DELETE' ? true : false ); Dispatcher::dispatch(); if (C('REQUEST_VARS_FILTER' )){ array_walk_recursive($_GET, 'think_filter' ); array_walk_recursive($_POST, 'think_filter' ); array_walk_recursive($_REQUEST, 'think_filter' ); } Hook::listen('url_dispatch' ); define('IS_AJAX' , ((isset ($_SERVER['HTTP_X_REQUESTED_WITH' ]) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH' ]) == 'xmlhttprequest' ) || !empty ($_POST[C('VAR_AJAX_SUBMIT' )]) || !empty ($_GET[C('VAR_AJAX_SUBMIT' )])) ? true : false ); C('TMPL_EXCEPTION_FILE' ,realpath(C('TMPL_EXCEPTION_FILE' ))); return ;}
TP的应用初始化可以分为以下几个阶段:
加载动态应用的文件和配置(8)
设置日志文件的绝对路径(11)
URL调度(13~34)
设置异常页面的模板文件(37)
应用开始函数:App::exec()
如果不是CLI环境的话,设置session
进入App::exec()函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 static public function exec () { if (!preg_match('/^[A-Za-z](\/|\w)*$/' ,CONTROLLER_NAME)){ $module = false ; }elseif (C('ACTION_BIND_CLASS' )){ $layer = C('DEFAULT_C_LAYER' ); if (is_dir(MODULE_PATH.$layer.'/' .CONTROLLER_NAME)){ $namespace = MODULE_NAME.'\\' .$layer.'\\' .CONTROLLER_NAME.'\\' ; }else { $namespace = MODULE_NAME.'\\' .$layer.'\\_empty\\' ; } $actionName = strtolower(ACTION_NAME); if (class_exists($namespace.$actionName)){ $class = $namespace.$actionName; }elseif (class_exists($namespace.'_empty' )){ $class = $namespace.'_empty' ; }else { E(L('_ERROR_ACTION_' ).':' .ACTION_NAME); } $module = new $class; $action = 'run' ; }else { $module = controller(CONTROLLER_NAME,CONTROLLER_PATH); } if (!$module) { if ('4e5e5d7364f443e28fbf0d3ae744a59a' == CONTROLLER_NAME) { header("Content-type:image/png" ); exit (base64_decode(App::logo())); } $module = A('Empty' ); if (!$module){ E(L('_CONTROLLER_NOT_EXIST_' ).':' .CONTROLLER_NAME); } } if (!isset ($action)){ $action = ACTION_NAME.C('ACTION_SUFFIX' ); } try { self ::invokeAction($module,$action); } catch (\ReflectionException $e) { $method = new \ReflectionMethod($module,'__call' ); $method->invokeArgs($module,array ($action,'' )); } return ; }
对传入的控制器进行安全检查
判断是否有ACTION_BIND_CLASS
配置
调用controller方法创建控制器实例
如果为空控制器,则显示Logo
如果没有动作的话,取出默认动作
最后调用invokeAction执行$module
类中的$action
方法(此处利用了php的反射机制)
到这里基本整个TP框架的初始化就完成了,接下来就是去依次根据路由执行各个控制器的方法
总结 在之前分析一些框架的时候,函数老是跳来跳去的,就把自己跟乱了,这次分析一边TP框架,每个框架都有他们相同的地方,现在大多数框架都是MVC模式,这些框架中的一般都会有URL调度,错误异常处理,DEBUG模式,缓存加载等通用的技术,可能实现的语言不同,但是逻辑和原理并不会差太多。
基本TP框架的流程可以大体上分为三个:
TP框架核心文件的加载和配置
应用核心文件的加载与配置
应用开始运行