看明白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_PATHRUNTIME_PATHLOG_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}

将错误、异常、中止时分别交由appErrorappExceptionappShutdown 处理,这三个函数在thinkphp/library/think/Error.php 定义。

接着是加载惯例配置文件

1\think\Config::set(include THINK_PATH . 'convention' . EXT);

也就是包含thinkphp/convention.php这个配置文件,将配置作为数组变量传入thinkphp/library/think/Config.php:160

image 可以通过字符串、数组的形式赋值。配置完之后返回 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_moduleBIND_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.phpsend()发送到客户端。

小结

App::run()是thinkphp程序的主要核心,在其中进行了初始化应用配置–>模块/控制器绑定–>加载语言包–>路由检查–>DEBUG记录–>exec()应用调度–>输出客户端,简单画了一个流程图 image

路由检查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}

首先$pathrequest实例拿到的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()返回的就是一个数组,数组中存放着模块控制器/操作 image 那么routeCheck()返回的$result会作为thinkphp/library/think/App.php:116$dispatch的值,进入到exec()的应用调度中。

小结

又臭又长的文字不如一张图 image

应用调度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']调度类型来进行区别处理,其中除了redirectresponse之外的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方法中 image 最后return的是就是我们的hello方法,但是此时的参数是空的,而我们传入有name=aaa参数,那么这个参数在哪赋值的呢?跟进反射看看 image 在339行,$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}

image 此时访问

1http://php.local/public/index.php?s=index/index/hello/name/aaa

页面则会返回 image 到此只是判断某个参数是否存在,是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的expbind注入就出自此处。再来看上文的强制类型转换$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()助手函数 ->requestparam() -> requestinput()获取参数 我们此时再来看下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)

image 为了方便看流程,我注释掉了部分代码,可以看到首先是$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获取到的就是渲染的内容,这就是全部流程。

小结

image

总结

thinkphp那么多的代码不是我一篇文章就能说完的,阅读thinkphp的源码你需要对thinkphp的开发流程及php的函数特性有着足够深入的了解,在本文中只是简单介绍了thinkphp的实现过程,有很多东西没有时间和精力去写笔记,比如模板解析、Model层、数据库交互、模板缓存等是怎么实现的,东西是写给自己看的,如果有前辈或者后人看到了这篇文章,请多谅解。

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。