技术改变世界,文化改变人心

TP框架源码分析

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)

  1. 定义了各种不同的url模式,在配置文件中可以更改
  2. 定义了类文件的后缀名为.class.php
  3. 定义了一些路径常量

    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/ 应用运行目录

  4. 判断是否为SAE环境,如果不是为普通模式,存储类型默认为FILE

  5. 定义各种核心类库,运行时目录等等在运行时需要的路径常量,都是与之前的路径常量拼接得到的

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) {
//CGI/FASTCGI模式下
$_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));
}
}

// 加载核心Think类
require CORE_PATH.'Think'.EXT;
  1. 通过PHP版本和函数判断GPC是否开启,在5.4.0版本以下可以通过get_magic_quotes_gpc()来判断GPC是否开启,在5.4.0以后GPC被废除,所以直接定义MAGIC_QUOTES_GPC为false
  2. 判断PHP的SAPI是否为CGI,是否为WIN或者CLI
  3. 如果不是CLI模式,并且没有定义_PHP_FILE_的话,就定义_PHP_FILE_的值为当前脚本的路径(即为index.php入口文件的路径)
  4. 定义__ROOT__的值为tp框架所在的文件夹
  5. 最后包含核心框架类

以下为各种常量在本机的值(在不同的机器上有所不同):

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
// 注册AUTOLOAD方法
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;
}
}
}
  1. 调用Log::save()将错误记录到日志中
  2. 通过error_get_last函数获得最后一次发生的错误
  3. 判断错误的类型
  4. 如果是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
/**
* 自定义错误处理
* @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();
$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;
}
}
  1. 和fatalErorr有些相像,先判断是那种类型的错误
  2. 如果是E_USER_ERROR的话,输出并清除缓冲区内容
  3. 如果设置了LOG_RECORD,则在日志文件中记录这次错误
  4. 调用halt函数将错误输出
  5. 如果不是错误而逝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
/**
* 自定义异常处理
* @access public
* @param mixed $e 异常对象
*/
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);
// 发送404信息
header('HTTP/1.1 404 Not Found');
header('Status:404 Not Found');
self::halt($error);
}

当发生异常并且没有处理的时候,调用appException

  1. 获取错误信息,错误路径,获取发生错误的方法,文件,行数
  2. 在日志文件中记录当前的错误信息
  3. 向用户发送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();
  1. 定义$runtimefile的目录:./Application/Runtime/common~runtime.php
  2. 如果不是DEBUG模式并且存在缓存文件的话,直接加载缓存文件
  3. 如果是DEBUG模式,如果缓存文件存在的话则删除
  4. 读取当前应用模式,默认为普通模式,由APP_MODE定义,当前为普通模式
  5. 加载类型别名定义,实际上是将命名空间映射成对应文件的绝对路径
  6. 加载模式行为定义
  7. 加载底层语言包
  8. 加载调试模式的配置文件
  9. 判断应用状态并读取状态配置文件(如果APP_STATUS常量定义不为空的话)
  10. 调用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');
// Session初始化
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
/**
* 应用程序初始化
* @access public
* @return void
*/
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);

// URL调度
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');
}

// URL调度结束标签
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);

// TMPL_EXCEPTION_FILE 改为绝对地址
C('TMPL_EXCEPTION_FILE',realpath(C('TMPL_EXCEPTION_FILE')));
return ;
}

TP的应用初始化可以分为以下几个阶段:

  1. 加载动态应用的文件和配置(8)
  2. 设置日志文件的绝对路径(11)
  3. URL调度(13~34)
  4. 设置异常页面的模板文件(37)

应用开始函数:App::exec()

  1. 如果不是CLI环境的话,设置session
  2. 进入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')){
// 操作绑定到类:模块\Controller\控制器\操作
$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;
// 操作绑定到类后 固定执行run入口
$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()));
}

// 是否定义Empty控制器
$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) {
// 方法调用发生异常后 引导到__call方法处理
$method = new \ReflectionMethod($module,'__call');
$method->invokeArgs($module,array($action,''));
}
return ;
}
  1. 对传入的控制器进行安全检查
  2. 判断是否有ACTION_BIND_CLASS配置
  3. 调用controller方法创建控制器实例
  4. 如果为空控制器,则显示Logo
  5. 如果没有动作的话,取出默认动作
  6. 最后调用invokeAction执行$module类中的$action方法(此处利用了php的反射机制)

到这里基本整个TP框架的初始化就完成了,接下来就是去依次根据路由执行各个控制器的方法

总结

在之前分析一些框架的时候,函数老是跳来跳去的,就把自己跟乱了,这次分析一边TP框架,每个框架都有他们相同的地方,现在大多数框架都是MVC模式,这些框架中的一般都会有URL调度,错误异常处理,DEBUG模式,缓存加载等通用的技术,可能实现的语言不同,但是逻辑和原理并不会差太多。

基本TP框架的流程可以大体上分为三个:

  1. TP框架核心文件的加载和配置
  2. 应用核心文件的加载与配置
  3. 应用开始运行