目录
ThinkPHP5.0完整请求简述
本文会对ThinkPHP5.0的一次完整请求(其实也不完整)进行记录,主要是在访问/
和index/index
进行记录。
载入文件及配置
base.php
index.php 定义应用目录,然后包含/../thinkphp/start.php文件
start.php会载入base.php文件,在base.php中会定义很多的环境变量:
Loader::register()
创建完环境变量后,就会载入\think\Loader::register()
之后加载根目录下的环境变量(如果有的话)
然后注册自动加载
register()
会自动调用spl_autoload_register()
并注册最基本的三个命名空间
注册命名空间的是使用PSR-4,它用来自动加载命名空间和文件系统
PSR-4的详情可以看文末的文文章链接
之后是加载类库映射文件,通常没有,但是下面的Composer自动加载支持才是重要的
registerComposerLoader()
先是载入三个常规的文件:
这三个文件也是文件包文件,每个文件内的文件都会被循环注册命名空间
载入第四个文件autoload_files的时候会有一点不同
上面的
__require_file()
就是require
,只是被封装了一下
autoload_files()里面的文件有:
其中第二个是验证码模块,这个模块在载入的时候会自动注册一个路由和一个验证模块:
在调用Route和Validate的时候就会触发autoload函数从而自动寻找并载入
这是因为之前被spl_autoload_register()
注册了
由spl_autoload_register()
注册的命名空间的类被调用的时候,会自动调用Loader中的autoload()
函数,从而实现自动include操作,非常高级。
当然之后也有很多地方触发这个autoload()函数
到此Loader::register()
函数就执行完毕了
错误异常处理及惯例配置
回到上面的base.php
文件之后就开始是注册一个错误的对象:\think\Error::register();
当然,在注册之前需要走一遍autoload流程从而载入Error对象
然后register()对象注册:
注意,注释中1开头表示笔者添加的注释,非官方的
注册之后就是base.php
的最后 一行指令,载入惯例配置convention.php
然后调用set函数将所有配置都载入到Config中的$config
数组中
到此第一部分—文件注册和配置载入就完成了
进行App::run()操作
首先就是调用\think\App
类进行例行的autoload
操作:
最终定位到文件\thinkphp\library\think\App.php
文件下,然后include
这个文件
接着就是进入run
函数,也就是最主要的操作
构造Request
进入函数第一步是初始化一个request函数,如果传入了ruquest函数就不用初始化了
传入之前还是一段autoload()
函数操作
最后定位到\thinkphp\library\think\Request.php
文件中,然后include
它
紧接着就是直接调用instance
函数
如果当前的$instance
不为空,就静态绑定一个$instance
然后返回这个$instance
这个地方意思就是说,第一次需要创建request的时候就会很正常的创建,但是之后如果还需要Request的时候就不需要创建新的,直接返回当前Request类型,该对象被存储在$instance
中
**__construct(): **
这里面显示对对象属性进行判断,确保传入的属性可以被赋值,然后循环赋值参数,接着判断$filter
参数是否有值,若无则调用配置文件中的默认设置
调试的时候发现,默认设置里啥也没有(无效配置)
initCommon()初始化
之后是一段try代码
首先是对自身进行初始化设置
initCommon()
显示判断是否需要初始化,不需要就直接return,这次调试中,是需要进行配置的
之后是申请app
命名空间到application
目录下
接着$init()
初始化,并把配置文件保存到$config
中
在init()
中首先定位模块目录,默认没有,就是空
然后到APP目录下即application目录下寻找并包含进来(当然,这个文件是没有的,所以就直接跳过)
接着是加载配置文件,也是区aplication目录下看有没有config.php
文件,然后load()
它
load()
的时候先判断是否是文件,是文件的时候,就把config.php
传入到Config::set()
函数中
set()
里一判断是是数组,拼接在一起,结束。
书会init()
函数中
之后是就是两个配置文件载入,一个是数据库载入,一个是额外拓展载入
额外载入时application\extra文件夹下的所有文件
之后载入引用状态配置,默认没有,就直接跳过
后面载入行为拓展,会把application目录下的tags.php
文件传给Hook对象的import()
方法(当然,首次使用Hook是需要autoload()
的
Hook::import()
默认循环add每个参数
Hook::add()
add函数也很简单,判断了下$behavier
然后unset一下接着就是拼接了
循环完之后import()
也就完成了
之后就直接include common.php
文件,由于$module
是空的,所以语言载入也就跳过了
最后返回配置文件
Config::get()
没有参数默认返回所有配置
书会initCommon()
函数,进过init()
函数的一顿载入后,现在我们几乎把所有的配置文件都保存起来了
之后开始配置debug模式
当然配置前,又是一顿
autoload()
载入Env类
这个get可以看出来,是通过传入的app初始配置和环境中的debug来判断是否需要开启debug
之后就是就是设置php.ini文件,然后开启debug模式
然后判断是否需要大一点的buffer
并创建一个输出缓冲区
接着判断是否有root_namespace命名空间,没有就创建一个命名空间
下面是载入额外文件,默认是thinkphp\helper.php
文件
这个helper.php是一堆判断,之前就执行过一遍:
后面是设置时区,很简单,重要的是这个Hook::listen()
函数
Hook已经导入了,然后就直接传入参数开始监听,下面看看是如何监听的:
首先是创建一个results
的空列表
接着是调用本地静态函数get()
并传入tag,也就是之前传入的app_init
,将得到的结果传入exec
方法,并判断是否需要中断行为。
先看下这个get
方法
很明显,get返回空,然后就导致后面直接触发break
接着就完成listen
了
最后把$init
设置为真,然后返回所有配置
到此initCommen()
就结束了
语言及调度信息
现在我们继续回到App::run()
函数中
请求从此开始逐渐进入高潮:模块/控制器处理阶段,
首先是一段绑定操作,原生thinkphp是没有这个绑定任何模块的,所以直接运行的时候就跳过了
详情可以看thinkphp模块设计
之后就是过滤函数filter()
函数的调用 ,传入缺省过滤配置(也就是空)
Request::filter()
函数也是非常简单,如果过滤为空就是获取默认过滤的配置(也是空)不然就修改本地的filter
属性
简简单单处理完之后也是些常规操作—载入语言,不过这次Lang类是第一次调用的,所以会触发autoload函数,然后重载一下,最终定位到: \thinkphp\library\think\Lang.php
位置
Lang::rang()
也是非常简单,存在传参就设置到$range
然后返回,不然就直接返回$range
默认语言就是中文
之后配置多语言,然后给Request设置语言
设置也是相当简单,直接就设置了,注意,人家返回的是Request
对象
但是如果直接调用就返回当前语言集合,这个地方两个名字重名了,很坑。
接着加载语言包:
调用Lang::load()
函数,返回的是当前语言集合(上一步刚设置完)
载入也是非常简单,没有太多设置的话,就是一个include
的过程
看下接下来的三个操作:
先是监听app_dispath
:但是这个监听也是同上面的监听,并没有实质的监听操作产生,具体情况可以查看关于钩子函数,回调函数的概念。
然后设置回调参数(null)
接着对未设置调度信息则进行 URL 路由检测
routeCheck()
进入后首先是调用传入的request请求的path()
方法或者$path
由于Reequest的path是空的,于是进行配置,
首先是取出配置中的url_html_suffix, 默认就是html
之后是调用Request的pathinfo()方法,这个方法是用来处理传入的参数的,让我们看看他是如何工作的
第一步是取出pathinfo
信息从配置文件中,并将该值赋值给$_SERVER['PATH_INFO']
,然后从配置信息中删除该值,在is_clil
模式下直接获得地址
接下来是分析path信息,但是我们的PATH_INFO是空的,于是就跳过了
直接返回就返回了/
后面我们传入参数了再来看这一步的详细信息。
书回path()
方法
前面我们获取了path下详细信息,接下来就是进行详细处理
主要是对url后缀处理,不过我们没有参数,就不受影响,直接返回
书回路由检测功能中:
前面获取了路由的信息,接下来先判断是否需要进行路由,如果本地$routeCheck
开启的,就检测,不然就去配置文件里查看是否需要路由检测,默认是开启的
首先是读取缓存runtime
中的路由表文件:,如果不存在就载入配置文件中的路由信息
载入则是先读取路由文件名字,然后对每个文件依次读取
具体依次的载入时通过Route::import()
实现的
首先打开后是一堆检查和设置,这些设置都是写在applocation下的route.php下,然后一下就载入完了。
接着继续routeCheck()
方法
前面完成路由检测第一步,接下来则是通过路由信息寻找url调度
进入Route::check()
方法前,我们看看此次传入的参数:新生成的request对象,路由地址(\),分界线(\),domain配置(false)
括号里是此次传入的参数
打开后是分隔符替换,把/
换成了|
先是路由别名检测,这里没有,就直接跳过了
然后获得请求方法,获取路由规则,这里直接调用了method
,所以直接返回GET
方法
回到routeCheck()
中
url绑定么有,所以直接俄就返回false
默认配置中又没有静态路由,所以就直接跳过了
最后就是路由规则检测checkRoute()
方法
对规则进行遍历,遍历时,先对规则进行赋值
后面是一个参数有效性检测的函数checkRoute()
这么多检测,但是直接就返回ture了
回到routeCheck()
中
后面一堆判断,都没通过,最近能用的是
这个CheckRule()
方法
虽然代码多,但是大多数都直接跳过,最近能用的函数是
match函数用来匹配规则:
首先是把路由和请求分成两个部分
然后遍历规则,寻找匹配到的规则,然后返回规则字符,这里我之后helper注册了路由,所以匹配不到,就直接返回了
出了match后路由匹配就结束了,剩下的就直接返回了
之后checkRule()
结束后,然后回到checkRoute()
之后路由检测也结束,直接返回,回到routeCheck()
中
进入最后一步,控制器匹配
进入Route::parsuUrl()
方法:
进入parsuUrl()
中,首先是判断是否有绑定,此次没有绑定,就直接跳过第一步
下一步是把url分隔符又换成了/
了
接着是对url进行修改为了更好的了解这个过程,我们修改请求为/index.php/Index/index ,修改后:
然后继续观察,由于没有绑定控制器,所以第一步跳过,后面是对信息的赋值,其中path的赋值调用了函数parseUrlPath()
,进入查看:
这个函数是对url进行处理的,第一步是对参数处理,可以看到匹配?
了匹配到后发送到$var
中
如果没有? 就对/
进行匹配
这个地方的
?
不是传入的参数
然后匹配/
,最后将控制器和操作分开然后返回
返回后直接进入匹配
后面这个\$autoSearch 默认就传入的false
,所以直接跳过
接下来就进入到这个解析额外参数的操作中了
parseUrlParams()
这个函数进来后先是对config的参数进行获取
这个参数默认是false
但是url是空的,所以直接跳过,设置当前的请求参数
Request::instance()->route($var)
这个instance()
函数之前分析过request的,然后就是route
函数
最后返回input的返回结果
之后就是配置路由
最后返回$route
结果
最后返回到routeCheck()
中
最后就是返回模式和模式内容
exec()
然后就回到了久违的App::run()
中了
然后就是一些调度信息的记录
然后缓存检查也没有其他东西,就是直接返回
后面就是调用分发,这个exec
就是执行操作,$data
就是最后需要进过处理后需要发送给用户的东西,也就是说这就是控制器执行部分了。
exec():
protected static function exec($dispatch, $config)
{
switch ($dispatch['type']) {
case 'redirect': // 重定向跳转
$data = Response::create($dispatch['url'], 'redirect')
->code($dispatch['status']);
break;
case 'module': // 模块/控制器/操作
$data = self::module(
$dispatch['module'],
$config,
isset($dispatch['convert']) ? $dispatch['convert'] : null
);
break;
case 'controller': // 执行控制器操作
$vars = array_merge(Request::instance()->param(), $dispatch['var']);
$data = Loader::action(
$dispatch['controller'],
$vars,
$config['url_controller_layer'],
$config['controller_suffix']
);
break;
case 'method': // 回调方法
$vars = array_merge(Request::instance()->param(), $dispatch['var']);
$data = self::invokeMethod($dispatch['method'], $vars);
break;
case 'function': // 闭包
$data = self::invokeFunction($dispatch['function']);
break;
case 'response': // Response 实例
$data = $dispatch['response'];
break;
default:
throw new \InvalidArgumentException('dispatch type not support');
}
return $data;
}
进入后是一个switch的一个操作,进行选择,我们进入是进入模块/控制器部分的,
然后直接调用module模块:
进入后先获取request的实例
然后进行多模块部署,首先进行模块部分参数的初始化
初始绑定模块无,就直接进行模块初始化
但是这里有个需要注意的,就是这个elseif
当我们传入的控制器不存在这个文件夹的时候,$available
就不更改,也就是默认是false,后面回传出错误
先通过module()
方法设置当前模块名:index
如果前面的$available
不可以,此处就直接返回404然后退出程序,我们此处index存在,所以不用管
然后用当前模块初始化配置,下面看一下具体配置流程:
首先就是设置模块文件位置(其实就是给index加了个\
后面就是加载初始化文件init.php, 模块文件config.php数据库配置文件database.php,以及扩展文件extar.php
这些文件的位置都在application/index下(是index模块),当然,这些文件都不存在,所以要么跳过,要么默认配置
包括后面这些都没有(语言包被定向到默认语言中文了)
最后就是返回所有配置文件,初始化结束,回到module()
方法下
接着module()
获取参数,然后是通过$controller
方法将获取的控制器和操作赋值给request
对象
然后启动module_init的监听事件
接着尝试执行控制器,对传入的参数进行分析:
// 默认的访问控制器层
'url_controller_layer' => 'controller',
// 控制器类后缀
'controller_suffix' => false,
// 默认的空控制器名
'empty_controller' => 'Error',
知道参数后我们进行载入操作:
controller()
方法十分简短,先是执行getModuleAndClass()
来解析模块和类名
里面有一步
会注册这个命名空间,然后返回出来,
最后得到的就是app\index\controllor\Index
和index
后面进入class_exists()
会唤起autoload()
来载入这个类,也就是控制器类
然后通过invokeClass()
反射来实例化一个类,然后依赖注入
之后控制器就设置完成了,也成功实例化了,然后也重新返回到module()
里面
后面则是对方法的执行
$instance
就是实例化的控制器对象
如果能存在这个方法,就设置$call
变量,不然就直接返回错误
然后还是监听操作的开始
最后通过invokeMethod()
来执行操作
先是确定反射对象,然后是通过bindParams()
方法绑定对象,
由于我们的方法是空的,所以自动获取请求变量
param()
方法如下:
先是获取请求类型,然后将路由参数和请求参数合并
之后会返回内容,返回使用的是input()
函数
这个函数也很有意思,已经碰到过多次了
他会将输入的字符内容取出(此处是进行过滤操作),然后进行过滤,其中这个过滤是支持过滤器的
这里有个过滤器很有意思,就是说将传入的两个参数用 call_user_func($filter, $value);
是一个经典的代码执行点,当用户输入控制此处时,即可rec
然后再回到invokeMethod()
位置,
debug模式,然后反射执行方法即Index/index操作
成功到达指定位置
接着直接返回到exec()
位置
然后break;
然后后exec()
结束,返回请求内容
然后回到run()
方法中
接下来清空实例化类,它的任务完成了
清空操作也是十分的简单粗暴,直接交给内存回收器了
接下来看如何输出到客户端,这时候就需要Response类登场了
首先判断输出的东西是不是一个Response
类,如果是的话就直接返回,进行下一步的链式操作;不空的话就调用isAjax()
函数,并使用其结果和之前的返回创建返回类型,其他则创建一个空的回复。
这个参数会对传入的内容进行分类,就本次回复,人家的分类就是html
然后就是Response::create()
首先还是常规的
autoload()
函数来载入文件
载入后进入create()
函数
上来就是对$type
的一次初始化,由于直接存在内容,所以就们没有变化
后面是对$type
的值进行变化,如果有只,就把他变成一个地址,不然就是空
$type
被这么一处理就变成了一个路径:think\response\Html
然后后面class_exists直接触发autoload()
函数,然后载入这个文件
载入后检查这个类,发现这个类不存在,然后创建它,执行else语句
接着就是创建一个新的对象
将传入的$data
赋值给respond
对象
然后将把其他只也一起赋值给相应的位置
最后返回这个Respond
对象
然后在run里监听app_end
最后返回Respond
对象
App::run()
结束
进行respond→send()操作
首先进入send操作页面:
首先还是一个监听,暂时就直接跳过
之后是一个getContent()
用来处理输出的数据
此方法很简单,content
默认是null,最后会给content
赋值,也就说明这是初始化操作
output默认就是直接输出
protected function output($data)
{
return $data;
}
后面进行判断是否合法,接着就是赋值
后面的Trace调试注入默认是关的,也就直接跳过
后面获得缓存也直接无,就直接跳过
后面检测头部信息和状态码并发送
最后echo $data
发送数据
然后就是监听发送结束
清空当此请求
不出意外,还是直接进行了一次
autoload()
也是直接就结束了
因本人能力有限,若文章出现了错误,还请斧正,不吝赐教。
PSR-4 自动加载规范 - 说明文档 | 全部规范 |《PHP PSR 标准规范》| PHP 技术论坛 (learnku.com)