看明白thinkphp5框架是怎么实现的
环境
thinkphp5.0.24
1"require": {
2 "php": ">=5.4.0",
3 "topthink/framework": "5.0.*"
4},
目录结构
1thinkphp/ 根目录
2 /application 应用目录
3 /index 应用index模块目录
4 command.php 命令行命令配置目录
5 config.php 应用配置文件
6 databse.php 应用数据库配置文件
7 route.php 应用路由配置文件
8
9 /public 入口目录
10 /static 静态资源目录
11 .htacess apache服务器配置
12 index.php 默认入口文件
13 robots.txt 爬虫协议文件
14 router.php php命令行服务器入口文件
15
16 /vendor composer安装目录
17 build.php 默认自动生成配置文件
18 composer.json composer安装配置文件
19 console 控制台入口文件
20
21/vendor/topthink/framework 框架核心目录
22 /extend 框架扩展目录
23 /lang 框架语言目录
24 /library 框架核心目录
25 /mode 框架模式目录
26 /tests 框架测试目录
27 /tpl 框架模板目录
28 /vendor 第三方目录
29 base.php 全局常量文件
30 convention.php 全局配置文件
31 helper.php 辅助函数文件
32 start.php 框架引导入口
33 think.php 框架引导文件
框架引导start.php
thinkphp为单程序入口,这是mvc框架的特征,程序的入口在public目录下的index.php
1// 定义应用目录
2define('APP_PATH', __DIR__ . '/../application/');
3// 加载框架引导文件
4require __DIR__ . '/../thinkphp/start.php';
require
引入thinkphp的start.php
1// ThinkPHP 引导文件
2// 1. 加载基础文件
3require __DIR__ . '/base.php';
4
5// 2. 执行应用
6App::run()->send();
在base.php(thinkphp/base.php)
中定义了一些常量,比如ROOT_PATH
、RUNTIME_PATH
、LOG_PATH
等等,然后引入Loader
类来自动加载
1thinkphp/base.php:37
2// 载入Loader类
3require CORE_PATH . 'Loader.php';
然后在下面通过.env
文件putenv环境变量,最后
1// 注册自动加载
2\think\Loader::register();
3
4// 注册错误和异常处理机制
5\think\Error::register();
6
7// 加载惯例配置文件
8\think\Config::set(include THINK_PATH . 'convention' . EXT);
在\think\Loader::register()
中,使用think\Loader::autoload
注册自动加载
1spl_autoload_register($autoload ?: 'think\\Loader::autoload', true, true);
当PHP引擎遇到试图实例化未知类的操作时,会调用
__autoload()
方法,并将类名当做字符串参数传递给它。spl_autoload_register
会将多个autoload
函数以数列的形式依次调用注册。
autoload()
的定义,通过名字来引入类
1public static function autoload($class)
2{
3 // 检测命名空间别名
4 if (!empty(self::$namespaceAlias)) {
5 $namespace = dirname($class);
6 if (isset(self::$namespaceAlias[$namespace])) {
7 $original = self::$namespaceAlias[$namespace] . '\\' . basename($class);
8 if (class_exists($original)) {
9 return class_alias($original, $class, false);
10 }
11 }
12 }
13
14 if ($file = self::findFile($class)) {
15 // 非 Win 环境不严格区分大小写
16 if (!IS_WIN || pathinfo($file, PATHINFO_FILENAME) == pathinfo(realpath($file), PATHINFO_FILENAME)) {
17 __include_file($file);
18 return true;
19 }
20 }
21
22 return false;
23}
注册命名空间定义
1self::addNamespace([
2 'think' => LIB_PATH . 'think' . DS,
3 'behavior' => LIB_PATH . 'behavior' . DS,
4 'traits' => LIB_PATH . 'traits' . DS,
5]);
加载类库映射文件
1if (is_file(RUNTIME_PATH . 'classmap' . EXT)) {
2 self::addClassMap(__include_file(RUNTIME_PATH . 'classmap' . EXT));
3}
注册错误和异常处理机制\think\Error::register()
1public static function register()
2{
3 error_reporting(E_ALL);
4 set_error_handler([__CLASS__, 'appError']);
5 set_exception_handler([__CLASS__, 'appException']);
6 register_shutdown_function([__CLASS__, 'appShutdown']);
7}
将错误、异常、中止时分别交由appError
、appException
、appShutdown
处理,这三个函数在thinkphp/library/think/Error.php
定义。
接着是加载惯例配置文件
1\think\Config::set(include THINK_PATH . 'convention' . EXT);
也就是包含thinkphp/convention.php
这个配置文件,将配置作为数组变量传入thinkphp/library/think/Config.php:160
thinkphp/start.php:19
启动程序
1// 2. 执行应用
2App::run()->send();
小结
thinkphp通过start.php
引入的base.php
定义文件夹等系统常量,然后引入Loader
来加载任意类,通过自动加载使用Error
类注册错误处理,以及Config
类加载模式配置文件thinkphp/convention.php
。做好一系列准备工作之后,执行应用 App::run()->send()
应用启动App::run()
在上文加载完配置等一系列工作之后,进入App::run()
,在run()
方法中
首先拿到Request
的一个实例,然后调用$config = self::initCommon()
初始化公共配置
1public static function initCommon()
2{
3 if (empty(self::$init)) {
4 if (defined('APP_NAMESPACE')) {
5 self::$namespace = APP_NAMESPACE;
6 }
7
8 Loader::addNamespace(self::$namespace, APP_PATH);
9
10 // 初始化应用
11 $config = self::init();
12 self::$suffix = $config['class_suffix'];
13
14 // 应用调试模式
15 self::$debug = Env::get('app_debug', Config::get('app_debug'));
16
17 if (!self::$debug) {
18 ini_set('display_errors', 'Off');
19 } elseif (!IS_CLI) {
20 // 重新申请一块比较大的 buffer
21 if (ob_get_level() > 0) {
22 $output = ob_get_clean();
23 }
24
25 ob_start();
26
27 if (!empty($output)) {
28 echo $output;
29 }
30
31 }
32
33 if (!empty($config['root_namespace'])) {
34 Loader::addNamespace($config['root_namespace']);
35 }
36
37 // 加载额外文件
38 if (!empty($config['extra_file_list'])) {
39 foreach ($config['extra_file_list'] as $file) {
40 $file = strpos($file, '.') ? $file : APP_PATH . $file . EXT;
41 if (is_file($file) && !isset(self::$file[$file])) {
42 include $file;
43 self::$file[$file] = true;
44 }
45 }
46 }
47
48 // 设置系统时区
49 date_default_timezone_set($config['default_timezone']);
50
51 // 监听 app_init
52 Hook::listen('app_init');
53
54 self::$init = true;
55 }
56
57 return Config::get();
58}
先Loader::addNamespace(self::$namespace, APP_PATH)
添加app所在的命名空间,然后初始化应用$config = self::init()
,然后根据self::$debug
决定是否将debug信息写入缓冲区,然后根据$config['extra_file_list']
的配置来加载额外的配置文件,然后设置时区,hook
回调app_init
,最后无参数调用Config::get()
返回所有全局配置
1//thinkphp/library/think/Config.php:120
2// 无参数时获取所有
3if (empty($name) && isset(self::$config[$range])) {
4 return self::$config[$range];
5}
初始化应用self::init()
的时候
1private static function init($module = '')
2{
3 // 定位模块目录
4 $module = $module ? $module . DS : '';
5
6 // 加载初始化文件
7 if (is_file(APP_PATH . $module . 'init' . EXT)) {
8 include APP_PATH . $module . 'init' . EXT;
9 } elseif (is_file(RUNTIME_PATH . $module . 'init' . EXT)) {
10 include RUNTIME_PATH . $module . 'init' . EXT;
11 } else {
12 // 加载模块配置
13 $config = Config::load(CONF_PATH . $module . 'config' . CONF_EXT);
14
15 // 读取数据库配置文件
16 $filename = CONF_PATH . $module . 'database' . CONF_EXT;
17 Config::load($filename, 'database');
18
19 // 读取扩展配置文件
20 if (is_dir(CONF_PATH . $module . 'extra')) {
21 $dir = CONF_PATH . $module . 'extra';
22 $files = scandir($dir);
23 foreach ($files as $file) {
24 if ('.' . pathinfo($file, PATHINFO_EXTENSION) === CONF_EXT) {
25 $filename = $dir . DS . $file;
26 Config::load($filename, pathinfo($file, PATHINFO_FILENAME));
27 }
28 }
29 }
30
31 // 加载应用状态配置
32 if ($config['app_status']) {
33 Config::load(CONF_PATH . $module . $config['app_status'] . CONF_EXT);
34 }
35
36 // 加载行为扩展文件
37 if (is_file(CONF_PATH . $module . 'tags' . EXT)) {
38 Hook::import(include CONF_PATH . $module . 'tags' . EXT);
39 }
40
41 // 加载公共文件
42 $path = APP_PATH . $module;
43 if (is_file($path . 'common' . EXT)) {
44 include $path . 'common' . EXT;
45 }
46
47 // 加载当前模块语言包
48 if ($module) {
49 Lang::load($path . 'lang' . DS . Request::instance()->langset() . EXT);
50 }
51 }
52
53 return Config::get();
54}
根据传入的$module
判断是模块还是整个应用需要初始化,如果是模块就包含APP_PATH . $module . 'init' . EXT
,也就是/application/init.php
,如果没传module
就包含application/config.php
,然后就是加载一些配置文件和语言包。
其实self::initCommon()
就是为了拿到全局的配置参数,继续看run
方法。
在拿到全局配置$config = self::initCommon();
之后,然后根据auto_bind_module
和BIND_MODULE
两个常量来决定是否需要自动绑定模块,绑定完之后进行了
1$request->filter($config['default_filter'])
设置当前的过滤规则,然后加载语言,监听app_dispatch
应用调度,获取应用调度信息,如果应用调度信息$dispatch
为空,则进行路由check
$dispatch = self::routeCheck($request, $config)
,路由check太多了,我拿出来写,然后记录当前调度信息$request->dispatch($dispatch)
,根据debug写日志,最后检查缓存之后执行了exec
函数拿到$data
作为response
的值,返回response
,而exec()
才是真正的应用调度函数,会根据$dispatch
的值来进入不同的调度模式,也单独拿出来说,至此App.php中就走完了,然后经过thinkphp/start.php
的send()
发送到客户端。
小结
App::run()
是thinkphp程序的主要核心,在其中进行了初始化应用配置–>模块/控制器绑定–>加载语言包–>路由检查–>DEBUG记录–>exec()应用调度–>输出客户端,简单画了一个流程图
路由检查self::routeCheck()
上文中我们说过,在未设置调度信息会进行URL路由检测
1if (empty($dispatch)) {
2 $dispatch = self::routeCheck($request, $config);
3}
跟进看下定义
1public static function routeCheck($request, array $config)
2{
3 $path = $request->path();
4 $depr = $config['pathinfo_depr'];
5 $result = false;
6
7 // 路由检测
8 $check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on'];
9 if ($check) {
10 // 开启路由
11 if (is_file(RUNTIME_PATH . 'route.php')) {
12 // 读取路由缓存
13 $rules = include RUNTIME_PATH . 'route.php';
14 is_array($rules) && Route::rules($rules);
15 } else {
16 $files = $config['route_config_file'];
17 foreach ($files as $file) {
18 if (is_file(CONF_PATH . $file . CONF_EXT)) {
19 // 导入路由配置
20 $rules = include CONF_PATH . $file . CONF_EXT;
21 is_array($rules) && Route::import($rules);
22 }
23 }
24 }
25
26 // 路由检测(根据路由定义返回不同的URL调度)
27 $result = Route::check($request, $path, $depr, $config['url_domain_deploy']);
28 $must = !is_null(self::$routeMust) ? self::$routeMust : $config['url_route_must'];
29
30 if ($must && false === $result) {
31 // 路由无效
32 throw new RouteNotFoundException();
33 }
34 }
35
36 // 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索
37 if (false === $result) {
38 $result = Route::parseUrl($path, $depr, $config['controller_auto_search']);
39 }
40
41 return $result;
42}
首先$path
是request
实例拿到的uri
路径,注意是从public
目录开始的uri路径,$depr
是config.php中定义的pathinfo
分隔符,然后进入if语句块,如果有路由缓存会读路由缓存,没有的话会读/application/route.php
导入路由,经过Route::check()
后,会拿$config['url_route_must']
来判断是否是强路由
1// 是否强制使用路由
2'url_route_must' => false,
如果是强路由会抛出throw new RouteNotFoundException()
异常,如果没有开启强路由会进入Route::parseUrl($path, $depr, $config['controller_auto_search'])
自动解析模块/控制器/操作/参数
先跟进到Route::check()
康康
1public static function check($request, $url, $depr = '/', $checkDomain = false)
2{
3 //检查解析缓存
4 if (!App::$debug && Config::get('route_check_cache')) {
5 $key = self::getCheckCacheKey($request);
6 if (Cache::has($key)) {
7 list($rule, $route, $pathinfo, $option, $matches) = Cache::get($key);
8 return self::parseRule($rule, $route, $pathinfo, $option, $matches, true);
9 }
10 }
11
12 // 分隔符替换 确保路由定义使用统一的分隔符
13 $url = str_replace($depr, '|', $url);
14
15 if (isset(self::$rules['alias'][$url]) || isset(self::$rules['alias'][strstr($url, '|', true)])) {
16 // 检测路由别名
17 $result = self::checkRouteAlias($request, $url, $depr);
18 if (false !== $result) {
19 return $result;
20 }
21 }
22 $method = strtolower($request->method());
23 // 获取当前请求类型的路由规则
24 $rules = isset(self::$rules[$method]) ? self::$rules[$method] : [];
25 // 检测域名部署
26 if ($checkDomain) {
27 self::checkDomain($request, $rules, $method);
28 }
29 // 检测URL绑定
30 $return = self::checkUrlBind($url, $rules, $depr);
31 if (false !== $return) {
32 return $return;
33 }
34 if ('|' != $url) {
35 $url = rtrim($url, '|');
36 }
37 $item = str_replace('|', '/', $url);
38 if (isset($rules[$item])) {
39 // 静态路由规则检测
40 $rule = $rules[$item];
41 if (true === $rule) {
42 $rule = self::getRouteExpress($item);
43 }
44 if (!empty($rule['route']) && self::checkOption($rule['option'], $request)) {
45 self::setOption($rule['option']);
46 return self::parseRule($item, $rule['route'], $url, $rule['option']);
47 }
48 }
49
50 // 路由规则检测
51 if (!empty($rules)) {
52 return self::checkRoute($request, $rules, $url, $depr);
53 }
54 return false;
55}
首先检查路由缓存,默认config.php中是不开启路由缓存的,然后检测路由别名
1private static $rules = [
2 'get' => [],
3 'post' => [],
4 'put' => [],
5 'delete' => [],
6 'patch' => [],
7 'head' => [],
8 'options' => [],
9 '*' => [],
10 'alias' => [],
11 'domain' => [],
12 'pattern' => [],
13 'name' => [],
14];
如果路由存在别名会进入checkRouteAlias()
,在这个函数内会直接进入到路由对应的模块/控制器/操作。如果不存在别名会继续检查,然后是获取当前请求类型的路由规则->检测域名部署checkDomain()
->检测URL绑定checkUrlBind()
,然后会判断是否是静态路由,如果是会返回parseRule()
,不然返回self::checkRoute($request, $rules, $url, $depr)
。
在这里我要提一手thinkphp的多种路由定义
定义方式 | 定义格式 |
---|---|
方式1:路由到模块/控制器 | ‘[模块/控制器/操作]?额外参数1=值1&额外参数2=值2…’ |
方式2:路由到重定向地址 | ‘外部地址’(默认301重定向) 或者 [‘外部地址’,‘重定向代码’] |
方式3:路由到控制器的方法 | ‘@[模块/控制器/]操作’ |
方式4:路由到类的方法 | ‘\完整的命名空间类::静态方法’ 或者 ‘\完整的命名空间类@动态方法’ |
方式5:路由到闭包函数 | 闭包函数定义(支持参数传入) |
因为多种路由模式的支持,所以程序的流程也不尽相同,我这里只分析第一种模块/控制器/操作
的形式。再看App::routeCheck()
,如果不是route.php定义的路由并且没有开启强路由会开始自动搜索控制器
1// 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索
2if (false === $result) {
3 $result = Route::parseUrl($path, $depr, $config['controller_auto_search']);
4}
最终程序会进入parseUrl()
来解析url,在parseUrl()
中会解析url参数parseUrlParams()
,这两个函数就不分析了,就是单纯的分割参数存储数组,最后会return一个['type' => 'module', 'module' => $route]
说的不是很明白,我这边直接访问
1http://php.local/public/index.php?s=index/index/index/id/1
那么可以看到parseUrl()返回的就是一个数组,数组中存放着模块控制器/操作
routeCheck()
返回的$result
会作为thinkphp/library/think/App.php:116
的$dispatch
的值,进入到exec()
的应用调度中。
小结
又臭又长的文字不如一张图
应用调度App::exec()
我们上文提到了routeCheck()
返回的$dispatch
会进入到exec()
函数中
1protected static function exec($dispatch, $config)
2{
3 switch ($dispatch['type']) {
4 case 'redirect': // 重定向跳转
5 $data = Response::create($dispatch['url'], 'redirect')
6 ->code($dispatch['status']);
7 break;
8 case 'module': // 模块/控制器/操作
9 $data = self::module(
10 $dispatch['module'],
11 $config,
12 isset($dispatch['convert']) ? $dispatch['convert'] : null
13 );
14 break;
15 case 'controller': // 执行控制器操作
16 $vars = array_merge(Request::instance()->param(), $dispatch['var']);
17 $data = Loader::action(
18 $dispatch['controller'],
19 $vars,
20 $config['url_controller_layer'],
21 $config['controller_suffix']
22 );
23 break;
24 case 'method': // 回调方法
25 $vars = array_merge(Request::instance()->param(), $dispatch['var']);
26 $data = self::invokeMethod($dispatch['method'], $vars);
27 break;
28 case 'function': // 闭包
29 $data = self::invokeFunction($dispatch['function']);
30 break;
31 case 'response': // Response 实例
32 $data = $dispatch['response'];
33 break;
34 default:
35 throw new \InvalidArgumentException('dispatch type not support');
36 }
37
38 return $data;
39}
在这个方法中会根据不同的$dispatch['type']
调度类型来进行区别处理,其中除了redirect
和response
之外的case语句块都会调用App内的静态方法通过反射实现调用模块/控制器/操作
1module调度类型的self::module() -> self::invokeMethod()
2controller调度类型的Loader::action() -> 进入App::invokeMethod()
3method调度类型的self::invokeMethod()
4function调度类型的self::invokeFunction()
看定义invokeMethod()
1public static function invokeMethod($method, $vars = [])
2{
3 if (is_array($method)) {
4 $class = is_object($method[0]) ? $method[0] : self::invokeClass($method[0]);
5 $reflect = new \ReflectionMethod($class, $method[1]);
6 } else {
7 // 静态方法
8 $reflect = new \ReflectionMethod($method);
9 }
10
11 $args = self::bindParams($reflect, $vars);
12
13 self::$debug && Log::record('[ RUN ] ' . $reflect->class . '->' . $reflect->name . '[ ' . $reflect->getFileName() . ' ]', 'info');
14
15 return $reflect->invokeArgs(isset($class) ? $class : null, $args);
16}
在invokeMethod()
中,创建反射方法$reflect = new \ReflectionMethod($class, $method[1]);
,获取反射函数$args = self::bindParams($reflect, $vars);
,接着记录日志后调用$reflect->invokeArgs(isset($class) ? $class : null, $args);
反射调用模块/控制器/操作
中的操作
。
为了方便解释我在index控制器创建了hello
方法
1public function hello($name)
2{
3 return 'hello' . $name;
4}
然后访问
1http://php.local/public/index.php?s=index/index/hello/name/aaa
此时模块调度进入module
的case语句
1case 'module': // 模块/控制器/操作
2 $data = self::module(
3 $dispatch['module'],
4 $config,
5 isset($dispatch['convert']) ? $dispatch['convert'] : null
6 );
7 break;
在module
方法中
hello
方法,但是此时的参数是空的,而我们传入有name=aaa
参数,那么这个参数在哪赋值的呢?跟进反射看看
$args = self::bindParams($reflect, $vars)
作为invokeArgs()
的反射参数
1private static function bindParams($reflect, $vars = [])
2{
3 // 自动获取请求变量
4 if (empty($vars)) {
5 $vars = Config::get('url_param_type') ?
6 Request::instance()->route() :
7 Request::instance()->param();
8 }
9
10 $args = [];
11 if ($reflect->getNumberOfParameters() > 0) {
12 // 判断数组类型 数字数组时按顺序绑定参数
13 reset($vars);
14 $type = key($vars) === 0 ? 1 : 0;
15
16 foreach ($reflect->getParameters() as $param) {
17 $args[] = self::getParamValue($param, $vars, $type);
18 }
19 }
20
21 return $args;
22}
args
会从Request::instance()->route()
或者Request::instance()->param();
获取,也就是request中获取。这样就实现了从url中达到动态调用模块/控制器/操作
的目的。
小结
应用调度就是这样完成他的使命,一个switch语句判断$dispatch['type']
,然后进入不同的处理,如果实现业务逻辑则会通过反射类调用相应的模块/控制器/操作
函数,拿到操作返回的数据之后整个exec()函数就结束了。最终继续执行App::run()方法返回response对象,进入send()方法返回给客户端,整个流程结束。
请求处理Request类
请求类处于thinkphp/library/think/Request.php
,众所周知的是thinkphp有助手函数input()来获取请求参数,本节说一下thinkphp中具体怎么实现的。
我们先来给一个控制器来做演示
1public function hello($name)
2{
3 if(input('?name')){
4 var_dump(input('?name'));
5 return input('name');
6 }else{
7 return '没有设置name参数!';
8 }
9}
助手函数input()可以这么写:
1input('param.name');
2input('param.');
3或者
4input('name');
5input('');
判断有没有传递某个参数可以用
1input('?get.id');
2input('?post.name');
我们打断点跟进下,进入到thinkphp/helper.php:121
1function input($key = '', $default = null, $filter = '')
2{
3 if (0 === strpos($key, '?')) {
4 $key = substr($key, 1);
5 $has = true;
6 }
7 if ($pos = strpos($key, '.')) {
8 // 指定参数来源
9 list($method, $key) = explode('.', $key, 2);
10 if (!in_array($method, ['get', 'post', 'put', 'patch', 'delete', 'route', 'param', 'request', 'session', 'cookie', 'server', 'env', 'path', 'file'])) {
11 $key = $method . '.' . $key;
12 $method = 'param';
13 }
14 } else {
15 // 默认为自动判断
16 $method = 'param';
17 }
18 if (isset($has)) {
19 return request()->has($key, $method, $default);
20 } else {
21 return request()->$method($key, $default, $filter);
22 }
23}
第一个if是为了来判断是否传递某个参数
1input('?get.id');
2input('?post.name');
这种写法,会进入request()->has($key, $method, $default)
,request()
方法会返回一个request
类的实例
1function request()
2{
3 return Request::instance();
4}
has()
方法会返回一个布尔值来决定是否传递了这个参数
1public function has($name, $type = 'param', $checkEmpty = false)
2{
3 if (empty($this->$type)) {
4 $param = $this->$type();
5 } else {
6 $param = $this->$type;
7 }
8 // 按.拆分成多维数组进行判断
9 foreach (explode('.', $name) as $val) {
10 if (isset($param[$val])) {
11 $param = $param[$val];
12 } else {
13 return false;
14 }
15 }
16 return ($checkEmpty && '' === $param) ? false : true;
17}
1http://php.local/public/index.php?s=index/index/hello/name/aaa
页面则会返回
input('?name')
这种语法,我们继续跟进input('name')
这种语法,他会进入
1return request()->$method($key, $default, $filter);
当没有包含?
或.
时,
1input('?name')
2input('?get.name')
会进入request()->$method($key, $default, $filter)
,此时会进入的就是request类中的param()
方法,跟进
1public function param($name = '', $default = null, $filter = '')
2{
3 if (empty($this->mergeParam)) {
4 $method = $this->method(true);
5 // 自动获取请求变量
6 switch ($method) {
7 case 'POST':
8 $vars = $this->post(false);
9 break;
10 case 'PUT':
11 case 'DELETE':
12 case 'PATCH':
13 $vars = $this->put(false);
14 break;
15 default:
16 $vars = [];
17 }
18 // 当前请求参数和URL地址中的参数合并
19 $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
20 $this->mergeParam = true;
21 }
22 if (true === $name) {
23 // 获取包含文件上传信息的数组
24 $file = $this->file();
25 $data = is_array($file) ? array_merge($this->param, $file) : $this->param;
26 return $this->input($data, '', $default, $filter);
27 }
28 return $this->input($this->param, $name, $default, $filter);
29}
param()
方法会将原生$_GET
、$_POST
等全局数组的参数合并到$this->param
,然后进入$this->input()
1public function input($data = [], $name = '', $default = null, $filter = '')
2{
3 if (false === $name) {
4 // 获取原始数据
5 return $data;
6 }
7 $name = (string)$name;
8 if ('' != $name) {
9 // 解析name
10 if (strpos($name, '/')) {
11 list($name, $type) = explode('/', $name);
12 } else {
13 $type = 's';
14 }
15 // 按.拆分成多维数组进行判断
16 foreach (explode('.', $name) as $val) {
17 if (isset($data[$val])) {
18 $data = $data[$val];
19 } else {
20 // 无输入数据,返回默认值
21 return $default;
22 }
23 }
24 if (is_object($data)) {
25 return $data;
26 }
27 }
28
29 // 解析过滤器
30 $filter = $this->getFilter($filter, $default);
31
32 if (is_array($data)) {
33 array_walk_recursive($data, [$this, 'filterValue'], $filter);
34 reset($data);
35 } else {
36 $this->filterValue($data, $name, $filter);
37 }
38
39 if (isset($type) && $data !== $default) {
40 // 强制类型转换
41 $this->typeCast($data, $type);
42 }
43 return $data;
44}
可以看出来input()
是用来接收参数,并且经过了一层filterValue()
过滤和$this->typeCast($data, $type)
强制类型转换
1private function filterValue(&$value, $key, $filters)
2{
3 $default = array_pop($filters);
4 foreach ($filters as $filter) {
5 if (is_callable($filter)) {
6 // 调用函数或者方法过滤
7 $value = call_user_func($filter, $value);
8 } elseif (is_scalar($value)) {
9 if (false !== strpos($filter, '/')) {
10 // 正则过滤
11 if (!preg_match($filter, $value)) {
12 // 匹配不成功返回默认值
13 $value = $default;
14 break;
15 }
16 } elseif (!empty($filter)) {
17 // filter函数不存在时, 则使用filter_var进行过滤
18 // filter为非整形值时, 调用filter_id取得过滤id
19 $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));
20 if (false === $value) {
21 $value = $default;
22 break;
23 }
24 }
25 }
26 }
27 return $this->filterExp($value);
28}
filterValue()
会使用$fileter
通过call_user_func来回调过滤,thinkphp5.x的rce就是覆盖此处的$filter
为system()来执行命令,最后会$filterExp
过滤关键字符
1public function filterExp(&$value)
2{
3 // 过滤查询特殊字符
4 if (is_string($value) && preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT LIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOT EXISTS|NOTEXISTS|EXISTS|NOT NULL|NOTNULL|NULL|BETWEEN TIME|NOT BETWEEN TIME|NOTBETWEEN TIME|NOTIN|NOT IN|IN)$/i', $value)) {
5 $value .= ' ';
6 }
7 // TODO 其他安全过滤
8}
thinkphp3.2.3的exp
和bind
注入就出自此处。再来看上文的强制类型转换$this->typeCast($data, $type)
1private function typeCast(&$data, $type)
2{
3 switch (strtolower($type)) {
4 // 数组
5 case 'a':
6 $data = (array)$data;
7 break;
8 // 数字
9 case 'd':
10 $data = (int)$data;
11 break;
12 // 浮点
13 case 'f':
14 $data = (float)$data;
15 break;
16 // 布尔
17 case 'b':
18 $data = (boolean)$data;
19 break;
20 // 字符串
21 case 's':
22 default:
23 if (is_scalar($data)) {
24 $data = (string)$data;
25 } else {
26 throw new \InvalidArgumentException('variable type error:' . gettype($data));
27 }
28 }
29}
此时可知 input()
助手函数 ->request
类param()
-> request
类input()
获取参数
我们此时再来看下request类,这个类中有很多函数,比如get()、post()、put()、env()、delete()等,其实他们最终都会流向input()
函数
1public function get($name = '', $default = null, $filter = '')
2{
3 if (empty($this->get)) {
4 $this->get = $_GET;
5 }
6 if (is_array($name)) {
7 $this->param = [];
8 $this->mergeParam = false;
9 return $this->get = array_merge($this->get, $name);
10 }
11 return $this->input($this->get, $name, $default, $filter);
12}
比如get()
会合并$_GET
数组中的参数然后传入input()
。
小结
Request类是一个获取请求类,thinkphp将多种请求的全局数组封装了一下,变为自己的函数,并且进行了过滤和强制类型转换,以此保证参数的安全性。
视图渲染View.php
1<?php
2
3namespace app\index\controller;
4
5use think\Controller;
6
7class Index extends Controller
8{
9 public function index($name)
10 {
11 $this->assign('name',$name);
12 return $this->fetch();
13 }
14}
写一个index方法来赋值变量并渲染模板,需要注意继承父类Controller
,不然没法使用assign和fetch。创建模板文件application/index/view/index/index.html
,内容为
1hello {$name}
然后我们来康康thinkphp是怎么实现的模板功能,打断点
1//thinkphp/library/think/Controller.php
2protected function assign($name, $value = '')
3{
4 $this->view->assign($name, $value);
5
6 return $this;
7}
跟进$this->view->assign()
1public function assign($name, $value = '')
2{
3 if (is_array($name)) {
4 $this->data = array_merge($this->data, $name);
5 } else {
6 $this->data[$name] = $value;
7 }
8 return $this;
9}
这个方法中把赋给模板的参数合并到$this->data
,然后返回进入$this->fetch()
,
1//thinkphp/library/think/Controller.php:118
2protected function fetch($template = '', $vars = [], $replace = [], $config = [])
3{
4 return $this->view->fetch($template, $vars, $replace, $config);
5}
继续跟进
1public function fetch($template = '', $vars = [], $replace = [], $config = [], $renderContent = false)
2{
3 // 模板变量
4 $vars = array_merge(self::$var, $this->data, $vars);
5
6 // 页面缓存
7 ob_start();
8 ob_implicit_flush(0);
9
10 // 渲染输出
11 try {
12 $method = $renderContent ? 'display' : 'fetch';
13 // 允许用户自定义模板的字符串替换
14 $replace = array_merge($this->replace, $replace, (array) $this->engine->config('tpl_replace_string'));
15 $this->engine->config('tpl_replace_string', $replace);
16 $this->engine->$method($template, $vars, $config);
17 } catch (\Exception $e) {
18 ob_end_clean();
19 throw $e;
20 }
21
22 // 获取并清空缓存
23 $content = ob_get_clean();
24 // 内容过滤标签
25 Hook::listen('view_filter', $content);
26 return $content;
27}
先开启缓冲区,然后定义变量用来存放用户自定义的需要替换的字符串,进入config()
函数中做渲染引擎初始化配置
1public function config($name, $value = null)
2{
3 if (is_array($name)) {
4 $this->template->config($name);
5 $this->config = array_merge($this->config, $name);
6 } elseif (is_null($value)) {
7 return $this->template->config($name);
8 } else {
9 $this->template->$name = $value;
10 $this->config[$name] = $value;
11 }
12}
然后进入$this->engine->$method($template, $vars, $config);
1public function fetch($template, $data = [], $config = [])
2{
3 if ('' == pathinfo($template, PATHINFO_EXTENSION)) {
4 // 获取模板文件名
5 $template = $this->parseTemplate($template);
6 }
7 // 模板不存在 抛出异常
8 if (!is_file($template)) {
9 throw new TemplateNotFoundException('template not exists:' . $template, $template);
10 }
11 // 记录视图信息
12 App::$debug && Log::record('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]', 'info');
13 $this->template->fetch($template, $data, $config);
14}
当没有传模板名时会使用$this->parseTemplate($template)
来自动搜索模板文件
1private function parseTemplate($template)
2{
3 ...
4 if ($this->config['view_base']) {
5 // 基础视图目录
6 $module = isset($module) ? $module : $request->module();
7 $path = $this->config['view_base'] . ($module ? $module . DS : '');
8 } else {
9 $path = isset($module) ? APP_PATH . $module . DS . 'view' . DS : $this->config['view_path'];
10 }
11
12 $depr = $this->config['view_depr'];
13 if (0 !== strpos($template, '/')) {
14 $template = str_replace(['/', ':'], $depr, $template);
15 $controller = Loader::parseName($request->controller());
16 if ($controller) {
17 if ('' == $template) {
18 // 如果模板文件名为空 按照默认规则定位
19 $template = str_replace('.', DS, $controller) . $depr . (1 == $this->config['auto_rule'] ? Loader::parseName($request->action(true)) : $request->action());
20 }
21 ...
22 }
23 }
24 ...
25 return $path . ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.');
26}
最后返回的就是E:\code\php\thinkphp\thinkphp5\public/../application/index\view\index\index.html
,这是默认的模板位置,然后debug之后又进入$this->template->fetch($template, $data, $config)
$this->data = $vars;
将参数合并到data
中,然后开启缓冲区,进入$this->storage->read($cacheFile, $this->data)
,然后输出$content
,最后$content
就是我们模板已经被解析过的内容。那么我们进入$this->storage->read()
看下
1public function read($cacheFile, $vars = [])
2{
3 $this->cacheFile = $cacheFile;
4 if (!empty($vars) && is_array($vars)) {
5 // 模板阵列变量分解成为独立变量
6 extract($vars, EXTR_OVERWRITE);
7 }
8 //载入模版缓存文件
9 include $this->cacheFile;
10}
会将我们的参数进行变量覆盖,然后包含缓存文件,也就是我们的模板文件,在包含的时候缓冲区就写入了渲染完成的模板的内容,而后$content
获取到的就是渲染的内容,这就是全部流程。
小结
总结
thinkphp那么多的代码不是我一篇文章就能说完的,阅读thinkphp的源码你需要对thinkphp的开发流程及php的函数特性有着足够深入的了解,在本文中只是简单介绍了thinkphp的实现过程,有很多东西没有时间和精力去写笔记,比如模板解析、Model层、数据库交互、模板缓存等是怎么实现的,东西是写给自己看的,如果有前辈或者后人看到了这篇文章,请多谅解。
文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。
评论