这里是用
thinkphp 3.2.0
版本的框架来做分析的
入口文件为 index.php
,是在项目的根目录下。这样其实是不安全的,用户可以直接访问你文件夹里的文件。
可能有小伙伴说,我可以通过 apache
或 nginx
进行限制。
这个是可以做到的,但是,每增加一个新的同级目录,就得修改一下服务器的配置,这是件很麻烦的事情,而且,可能会遗忘。(如果没有这样的场景,当我没有说)同时,这也给服务器的配置文件加了N个限制规则,这个大家自己做取舍。
一般的公司都会将 index.php
放在一个独立的目录中,比如:public
。现在许多框架都是如此做的,thinkphp 5
已经改进了。这里不多做阐述。
入口文件
入口文件很简单,PHP
版本校验、定义 DEBUG
模式、定义应用目录、引入框架文件。到此就OK了。
// 应用入口文件
// 检测PHP环境
if(version_compare(PHP_VERSION,'5.3.0','<')) die('require PHP > 5.3.0 !');
// 开启调试模式 建议开发阶段开启 部署阶段注释或者设为false
define('APP_DEBUG', true);
// 定义应用目录
define('APP_PATH', './Application/');
// 引入ThinkPHP入口文件
require './ThinkPHP/ThinkPHP.php';
// 亲^_^ 后面不需要任何代码了 就是如此简单
现在我们来进入 ThinkPHP/ThinkPHP.php
里面看看 TP
都做了些什么。
ThinkPHP.php
ThinkPHP/ThinkPHP.php
这里只是做一些阐述,不做代码方面的分析。
- 记录开始运行时间
- 记录内存初始化
- 版本信息
- URL模式常量定义,
普通模式
、PATHINFO模式
、REWRITE模式
、兼容模式
- 文件后缀
.class.php
常量定义 - 目录常量定义,
系统运行时目录
、系统核心类库目录
、Think类库目录
、行为类库目录
、系统应用模式目录
...(太多了,就不做阐述了,可以直接看源码) - 检查运行环境,
CGI
、Windows
、cli
- 定义
C
函数,用于获取和设置配置 - 加载核心类
ThinkPHP/Library/Think/Think.class.php
启动应用
回顾一下,就是一些常量的初始化、环境判断、初始函数定义、框架启动。
启动框架
ThinkPHP/Library/Think/Think.class.php@start
注册 autoload
按照一定规则,实现类的自动加载,主要运用了 spl_autoload_register
。
想要了解这个函数,建议参照官方的文档 autoload。
之前也写过怎么写一个自动加载类,有兴趣的可以看看 自动加载模块。
目前写的比较好的是 composer
的自动加载,里面有一些优化的算法,有兴趣的可以研究研究。
设定错误和异常处理
当程序结束时,会执行用户自行注册的回调处理,这个注册回调的函数就是 register_shutdown_function
。
不过它也有一些限制,详细的还是参照官方文档 register_shutdown_function。
初始化系统错误和异常的接收,使用 set_error_handler
和 set_exception_handler
来接收处理,还记得 TP
的异常界面不,那就是这里处理的。
初始化文件存储方式
这个主要是来存储一些缓存的内容,比如:配置文件、模板编译文件、运行日志等。
它默认支持两种存储方式:File
和 Sae
。其中,File
就是本地文件存储的意思,默认是 File
。
配置文件的加载
这里的配置文件包含 TP
自身的文件和用户自定义的文件。
一开始根据 APP_DEBUG
来判断是否需要读取缓存的配置文件。
- 如果是
true
,则实时加载一系列配置文件,最后将这些配置合并成一个文件,存入缓存。 - 如果是
false
并且有了缓存文件了,则读取缓存之后的配置。
那么问题来了,如果生产环境配置的 APP_DEBUG
是 false
,如果某次上线,更改了配置文件之后,需要手动删除缓存文件,才能够重新生成新的配置文件。所以有些公司哪怕在线上,都是用的 APP_DEBUG = true
的模式。
好了,关于 APP_DEBUG
相关的问题阐述结束之后,要开始按照流程来解析代码了。
1、加载核心的配置文件 core.php
。
// CONF_PATH 是应用的配置文件目录,官方 demo 中的路径是 Application/Common/core.php
// MODE_PATH 是框架的目录,官方 demo 中的路径是 ThinkPHP/Mode
// APP_MODE 是当前的运行环境,主要用于 sae 环境辨别,一般情况下是 common
$mode = include is_file(CONF_PATH . 'core.php') ? CONF_PATH . 'core.php' : MODE_PATH . APP_MODE . '.php';
框架会优先加载应用的 CONF_PATH . 'core.php'
,这里的 CONF_PATH
是应用中的 Common
目录,当然,这个是可以自定义的。就 demo
而言,路径是 Application/Common/core.php
。
如果应用没有配置 core.php
,那么 TP
会加载框架内部核心配置,一般情况下,都会加载 ThinkPHP/Mode/common.php
文件,如果是 sae
环境,则会加载 ThinkPHP/Mode/sae.php
。
2、引入核心配置文件之后,会加载一系列框架的文件。(建议大致看一下)
foreach ($mode['core'] as $file){
if(is_file($file)) {
include $file;
if(!APP_DEBUG) $content .= compile($file);
}
}
// 系统函数,一些常用的 I、M、D、E 等会在里面定义
ThinkPHP/Common/functions.php
// 应用的自定义函数
Application/Common/function.php
// 系统钩子,应用的地方蛮多,会单独讲
ThinkPHP/Library/Think/Hook.class.php
// App
ThinkPHP/Library/Think/App.class.php
// 调度器,用于路由的调度
ThinkPHP/Library/Think/Dispatcher.class.php
// 路由解析器,用于解析
ThinkPHP/Library/Think/Route.class.php
// 基础控制器
ThinkPHP/Library/Think/Controller.class.php
// 视图类,直接与模板引擎交互的模块
ThinkPHP/Library/Think/View.class.php
// 一些系统行为,这个是个好东西
ThinkPHP/Library/Think/Behavior.class.php
ThinkPHP/Library/Behavior/ReadHtmlCacheBehavior.class.php
ThinkPHP/Library/Behavior/ShowPageTraceBehavior.class.php
ThinkPHP/Library/Behavior/ParseTemplateBehavior.class.php
ThinkPHP/Library/Behavior/ContentReplaceBehavior.class.php
ThinkPHP/Library/Behavior/WriteHtmlCacheBehavior.class.php
深思
看到这里之后,我就想到了我们的
api
项目,里面并没有自定义应用自己的core.php
文件,导致了一些不必要的文件的加载,比如:视图类、一些跟视图相关的行为类。
由于对于框架的不了解,从而导致了一些资源的滥用。这就是后续系统的可优化点。
3、加载一些 配置文件
。
foreach ($mode['config'] as $key => $file){
is_numeric($key) ? C(include $file) : C($key, include $file);
}
// 系统惯例配置,内容太多,不做解析,后面用到再说
ThinkPHP/Conf/convention.php
// 应用的配置,这个文件必须存在,框架没有做文件是否存在的校验
Application/Common/config.php
这里涉及了框架自定义的 C
函数,小伙伴们可以看一下 C
的实现方式。这边我想要强调的一点是后续相同的配置会覆盖之前的配置。
也就是说,ThinkPHP/Conf/convention.php
中的部分配置会被 Application/Common/config.php
所覆盖。这个符合我们的应用场景。
4、加载别名定义
// 加载模式别名定义
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');
}
这个主要是用于类自动加载的。
TP
会先加载模式(common)的 alias
,之后会加载应用的 Application/Common/alias.php
,这里后续的配置同样会覆盖之前的,这点需要注意一下。
5、加载行为定义
// 加载模式行为定义
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');
}
这个主要是用于定义一些系统行为,定义在某个阶段做的事情。这里运用了系统编写的钩子,如果想要提前了解的,可以先看一下,建议先看一下整个流程,再做详细的分析。
同样的,TP
会先加载模式(common)的 tags
,然后会加载应用的 Application/Common/tags.php
。
值得提的一点是,我们可以指定是全部覆盖,还是下一级覆盖,这里后续会讲解。
6、加载语言包
L(include THINK_PATH . 'Lang/' . strtolower(C('DEFAULT_LANG')) . '.php');
这个没什么可以说的,字面意思。
7、debug
模式的判定
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),'runtime');
}else{
// 调试模式加载系统默认的配置文件
C(include THINK_PATH.'Conf/debug.php');
// 读取应用调试配置文件
if(is_file(CONF_PATH.'debug.php'))
C(include CONF_PATH.'debug.php');
}
如果不是 debug
模式,那么会生成上述加载文件的总缓存,下次执行,直接读取缓存里的配置。
如果是 debug
模式,那么会加载 debug.php
。同样先读取系统配置,再读取应用配置,合并的时候,后者会覆盖前者部分配置。
8、读取当前应用状态对应的配置文件
if(APP_STATUS && is_file(CONF_PATH.APP_STATUS.'.php')) {
C(include CONF_PATH.APP_STATUS.'.php');
}
这个就很厉害了,用的好的话,C
函数的覆盖特性可以更好的帮助我们的开发。
9、设置系统时区
date_default_timezone_set(C('DEFAULT_TIMEZONE'));
由于部分服务器本身的问题,可能会存在时区上的问题,这里指定时区,可以获取正确的时间。
可以配置哦。
10、检查应用目录结构
if(C('CHECK_APP_DIR') && !is_dir(LOG_PATH)) {
// 创建应用目录结构
require THINK_PATH.'Common/build.php';
}
这个是 TP
自带的,还是蛮方便的,当然,需要的权限也是蛮多的,如果没有相关权限,这个执行会失败,一般情况下,会被关闭。
而且,这个每次都会判断一次,是一个无用的判断,浪费资源的,这个可以直接删除。
11、应用启动
App::run();
应用启动
看了上述的源码之后,已经大致对于框架加载了哪些内容有了个大致的了解。
执行到这里,框架该加载的已经加载了,配置也已经初始化完毕,后续的就是路由相关的解析和调度了。
运用 IDE
的代码追踪,我们可以看到有两个 App.class.php
,那么应该加载的是哪个呢?小伙伴们大致可以猜测到是执行的哪一个,从上面源码阅读中,也可以确认是 ThinkPHP/Library/Think/App.class.php
。
钩子app_init
// 应用初始化标签
Hook::listen('app_init');
一开始就执行了钩子 app_init
,至于要执行什么,是我们之前的配置,还记得上面的 tags
不,就是那里的配置。
我们可以自定义一些需要在应用启动之前要做的事情。
动态加载扩展文件
// 加载动态应用公共文件和配置
// COMMON_PATH 指的是应用下的 `Common` 目录
load_ext_file(COMMON_PATH);
如果有一些自定义的文件,没有按照 TP
的规则配置,或者有一部分文件想要动态加载,可以使用现有的配置。
LOAD_EXT_FILE
的配置可以加载 COMMON_PATH . 'Common'
目录下的文件。
LOAD_EXT_CONFIG
的配置可以加载 COMMON_PATH . 'Conf'
目录下的文件。
调度
Dispatcher::dispatch();
这个里面牵扯的比较多,单独来讲,主要是解析路由,获取模块、控制器、方法,解析参数。
定义一些系统常量
比如请求时间、请求方式等。
钩子url_dispatch
Hook::listen('url_dispatch');
一些配置的改变
改变 LOG_PATH
和 TMPL_EXCEPTION_FILE
(模板文件)
钩子app_begin
Hook::listen('app_begin');
session初始化
if(!IS_CLI){
session(C('SESSION_OPTIONS'));
}
这个是一个好东西啊,可以看看 session
方法的实现。
如果设置了 SESSION_TYPE
,我们可以指定 session
的驱动为 Db
。这也是 TP
提供的,所以啊,如果想要存到 redis
或者 memcache
里,就得自己写了。
执行应用程序
App::exec();
1、获取模块
- 检测控制器是否符合要求
- 如果符合要求,检查是否有绑定到类(这个一般的场景用不着,就不做讨论了,有需要可以自行看)
- 如果上述的判断都不通过,最终实例化控制器
2、模块实例化检查
if(!$module) {
// 是否定义Empty控制器
$module = A('Empty');
if(!$module){
E(L('_CONTROLLER_NOT_EXIST_').':'.CONTROLLER_NAME);
}
}
如果实例化有问题,检查是否有 Empty
控制器,如果有,则实例化 Empty
控制器,否则抛异常。
3、获取操作名
if(!isset($action)){
$action = C('ACTION_NAME')?C('ACTION_NAME'):ACTION_NAME;
$action .= C('ACTION_SUFFIX');
}
有则直接使用,没有则取动态路由,然后加上后缀。
4、检查操作名是否合法
if(!preg_match('/^[A-Za-z](\w)*$/',$action)){
// 非法操作
throw new \ReflectionException();
}
不合法直接抛异常。
5、反射获得当前的方法类
如果是 public
函数并且不是 static
函数。
- 检查
_before_
前置函数(这个方法是没有后缀的,没参数),有并且是public
就执行 - 检查是否有参数
- 没有就直接执行
- 有先获取传的参数(不区分
post
和get
),根据方法的参数名来拼接参数,并执行
- 检查
_after_
后置函数(这个方法是没有后缀的,没参数),有并且是public
就执行
6、异常处理
// 方法调用发生异常后 引导到__call方法处理
$method = new \ReflectionMethod($module,'__call');
$method->invokeArgs($module,array($action,''));
在执行控制器方法的过程中,任何一个抛了异常,都会重新通过 Controller.class.php
里面的 __call
方法重试一遍。
这个里面有个很神奇的逻辑,如果方法名和加了后缀的方法名一致(忽略大小写),一般是一致的,那么就会有几个判断:
method_exists($this,'_empty')
:如果 _empty
存在,则执行 _empty
,很友好。
file_exists_case($this->view->parseTemplate())
:如果模板是存在的,不用写控制器的说,真好啊(未尝试)。
最后,上面两个判断没有通过,直接抛异常了。
7、控制器里的钩子
实例化的时候,会执行 Hook::listen('action_begin',$this->config);
。
销毁的时候,会执行 Hook::listen('action_end');
8、钩子app_end
Hook::listen('app_end');
Think\Think::fatalError
还记得一开始说的 register_shutdown_function
函数不,这里就是最后的执行了。
整个 TP
的执行流程结束,当然里面还是有一些细节没有讲,这个会在之后的章节里细化。
回顾
看起来,流程很顺是不是?
理论上是的,可是 TP
的设计会让里面的一些设计直接会致使后续的一些流程直接断掉。
比如,我们写的接口基本在控制层就直接 exit()
了,也就是说,后面的 action_end
和 app_end
两个钩子会直接断掉,走不到。
哪怕就是官方在 ThinkPHP/Library/Think/Controller.class.php
中提供的 ajaxReturn
也是直接 exit()
的。
综合而言,符合了不少开发场景的要求,是一个不错的框架。