探究thinkphp URL路由流程

thinkphp源码浅析-3.视图层解析流程

1、建立控制器方法

  • 路径 thinkphp_5.0.7_core/application/index/controller/test.php
  • 代码 建立一个测试方法
    public function test($name = "")
    {
        $this->assign('name',$name);
        $res =  view('test');
        return $res;
        //$this->display();
    }

2、建立视图模板文件

  • 路径 thinkphp_5.0.7_core/application/index/view/test/test.html
  • 代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<hr />
项目更目录:__ROOT__
<h1>{$name}</h1>
</body>
</html>

3、目标 :

  • 同过test控制器test方法:接收参数name,并将变量$name 赋值给视图模板层输出,探究其中运行即解析流程。

4、运行流程

  • 4.1 入口文件index.php
// 定义应用目录
define('APP_PATH', __DIR__ . '/../application/');
// 加载框架引导文件
require __DIR__ . '/../thinkphp/start.php';
  • 4.2 start.php
require __DIR__ . '/base.php';
// 执行应用
App::run()->send();
  • 4.2.1 App::run() 应用主体运行
  • 4.2.1.1 实例化Request
is_null($request) && $request = Request::instance();
  • 4.2.1.2 初始化公共配置
$config = self::initCommon();
  • 4.2.1.3 默认路由绑定
if (defined('BIND_MODULE')) {
    // 模块/控制器绑定
    BIND_MODULE && Route::bind(BIND_MODULE);
} elseif ($config['auto_bind_module']) {
    // 入口自动绑定
    $name = pathinfo($request->baseFile(), PATHINFO_FILENAME);
    if ($name && 'index' != $name && is_dir(APP_PATH . $name)) {
        Route::bind($name);
    }
}
  • 4.2.1.4 请求过滤
$request->filter($config['default_filter']);
  • 4.2.1.5 多语言设置与加载
if ($config['lang_switch_on']) {
    // 开启多语言机制 检测当前语言
    Lang::detect();
} else {
    // 读取默认语言
    Lang::range($config['default_lang']);
}
$request->langset(Lang::range());
// 加载系统语言包
Lang::load([
    THINK_PATH . 'lang' . DS . $request->langset() . EXT,
    APP_PATH . 'lang' . DS . $request->langset() . EXT,
]);
  • 4.2.1.6 应用调度路由解析
$dispatch = self::$dispatch;
if (empty($dispatch)) {
    // +----------------------------------------------------------------------
    // | self::routeCheck($request, $config) 开始路由
    // +----------------------------------------------------------------------
    // 进行URL路由检测
    $dispatch = self::routeCheck($request, $config);
}
// 记录当前调度信息
$request->dispatch($dispatch);
// 记录路由和请求信息
if (self::$debug) {
    Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info');
    Log::record('[ HEADER ] ' . var_export($request->header(), true), 'info');
    Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info');
}
  • self::routeCheck($request, $config) 开始路由 thinkphp具体路由实现步骤存这里开始 源码浅析一、路由
  • 4.2.1.7 监听app_begin
Hook::listen('app_begin', $dispatch);
  • 4.2.1.8 请求缓存检查
$request->cache($config['request_cache'], $config['request_cache_expire'], $config['request_cache_except']);
  • 4.2.1.9 根据调度类型执行方法 跳转页面、控制器/方法加载等
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':
        $data = $dispatch['response'];
        break;
    default:
        throw new \InvalidArgumentException('dispatch type not support');
}
} catch (HttpResponseException $exception) {
$data = $exception->getResponse();
}
  • 这里类型为 module 执行对应映射控制器、方法返回方法执行内容
  • 映射时 会初始化一个视图instance => 初始化构造方法->初始化模板引擎$this->engine((array) $engine);
//path thinkphp_5.0.7_core/thinkphp/library/think/View.php

/**
 * 初始化视图
 * @access public
 * @param array $engine  模板引擎参数
 * @param array $replace  字符串替换参数
 * @return object
 */
public static function instance($engine = [], $replace = [])
{
    if (is_null(self::$instance)) {
        self::$instance = new self($engine, $replace);
    }
    return self::$instance;
}
  • 执行时 $res = view('test'); 创建一个Response对象 代码 路径thinkphp_5.0.7_core/thinkphp/helper.php
if (!function_exists('view')) {
    /**
     * 渲染模板输出
     * @param string    $template 模板文件
     * @param array     $vars 模板变量
     * @param array     $replace 模板替换
     * @param integer   $code 状态码
     * @return \think\response\View
     */
    function view($template = '', $vars = [], $replace = [], $code = 200)
    {
        return Response::create($template, 'view', $code)->replace($replace)->assign($vars);
    }
}
  • 4.2.1.10 清空类的实例化
Loader::clearInstance();
  • 4.2.1.11 输出数据到客户端
if ($data instanceof Response) {
            $response = $data;
        } elseif (!is_null($data)) {
            // 默认自动识别响应输出类型
            $isAjax   = $request->isAjax();
            $type     = $isAjax ? Config::get('default_ajax_return') : Config::get('default_return_type');
            $response = Response::create($data, $type);
        } else {
            $response = Response::create();
        }
  • 4.2.1.12 监听app_end
Hook::listen('app_end', $response);
  • 4.2.1.13 返回
return $response;
  • 4.2.2 App::run()->send(); 发送数据到客户端
  • 都数据返回给客户端了?那么内容标签解析呢?哪里执行的?这里说明下如果控制器使用的是printecho等直接输出打印的,那么映射控制器方法的时候是会直接接收到内容,其它返回还需接收处理(模板标签替换也在这块执行)。接下来流程:接收处理数据->编辑header(状态码、信息头等) -> 输出内容->用户端接收响应。
  • 4.2.2.1 输出数据处理
$data = $this->getContent();  // 返回html页面内容
  • getContent()代码
/**
 * 获取输出数据
 * @return mixed
 */
public function getContent()
{
    if (null == $this->content) {
        // +----------------------------------------------------------------------
        // |  $this->data
        // |      'test'  //这里数据就是在控制器方法中的view方法中创建response 时初始化赋值的数据
        // +----------------------------------------------------------------------

        $content = $this->output($this->data);
        // +----------------------------------------------------------------------
        // | $content 返回html
        // +----------------------------------------------------------------------
        if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable([
            $content,
            '__toString',
        ])
        ) {
            throw new \InvalidArgumentException(sprintf('variable type error: %s', gettype($content)));
        }

        $this->content = (string) $content;
    }
    return $this->content;
}
  • 接着output()
//path thinkphp_5.0.7_core/thinkphp/library/think/Response.php
 $content = $this->output($this->data);
  • 这里需注意,虽然think\Response 当前自身类中有output()方法,但是却不是执行自身的output()方法,而是由于在run()方法,映射类的时候test方法view时已经创建了实例,~~如果控制器中未实例化view实例在是在通过 Response::create()创建了think\response\View实例,~~后续操作的也是view实例,所以这里的$this->output($this->data) 自然操作的是think\response\View类的putout()方法^_^
  • 4.2.2.2 初始化视图 ViewTemplate::instance
//path think\View   instance()
ViewTemplate::instance(Config::get('template'), Config::get('view_replace_str'));// ->fetch($data, $this->vars, $this->replace);
  • 4.2.2.3 渲染模板文件 fetch
//path think\View   fetch()
/**
 * 解析和获取模板内容 用于输出
 * @param string    $template 模板文件名或者内容
 * @param array     $vars     模板输出变量
 * @param array     $replace 替换内容
 * @param array     $config     模板参数
 * @param bool      $renderContent     是否渲染内容
 * @return string
 * @throws Exception
 */
public function fetch($template = '', $vars = [], $replace = [], $config = [], $renderContent = false)
{
    // 模板变量
    $vars = array_merge(self::$var, $this->data, $vars);
    // +----------------------------------------------------------------------
    //        array (size=1)
    //  'name' => string 'hello' (length=5)
    // +----------------------------------------------------------------------


    // 页面缓存
    ob_start();
    ob_implicit_flush(0);

    // 渲染输出
    $method = $renderContent ? 'display' : 'fetch';
    // +----------------------------------------------------------------------
    // |    fetch
    // +----------------------------------------------------------------------
    //var_dump($method); //fetch
   $res = $this->engine->$method($template, $vars, $config);
   // var_dump($this->engine); //thinkphp_5.0.7_core/thinkphp/library/think/View.php
    // 获取并清空缓存
    $content = ob_get_clean();
    // 内容过滤标签
    Hook::listen('view_filter', $content);
    // 允许用户自定义模板的字符串替换

    $replace = array_merge($this->replace, $replace);
    // +----------------------------------------------------------------------
    //  $this->replace
    //        array (size=5)
    //  '__ROOT__' => string '/thinkphp_5.0.7_core/public' (length=27)
    //  '__URL__' => string '/thinkphp_5.0.7_core/public/index.php/index/test' (length=48)
    //  '__STATIC__' => string '/thinkphp_5.0.7_core/public/static' (length=34)
    //  '__CSS__' => string '/thinkphp_5.0.7_core/public/static/css' (length=38)
    //  '__JS__' => string '/thinkphp_5.0.7_core/public/static/js' (length=37)
    // +----------------------------------------------------------------------
    if (!empty($replace)) {
        // +----------------------------------------------------------------------
        // |  // 系统标签 或 自定义配置标签在此处替换
        // +----------------------------------------------------------------------
        $content = strtr($content, $replace);
    }
    return $content;
}
  • 模板处理

  • $res = $this->engine->$method($template, $vars, $config); 这里$method为fetch

 path think\Template fetch()
 /**
 * 渲染模板文件
 * @access public
 * @param string    $template 模板文件
 * @param array     $vars 模板变量
 * @param array     $config 模板参数
 * @return void
 */
public function fetch($template, $vars = [], $config = [])
{
    //var_dump($template,$vars,$config);
    // +----------------------------------------------------------------------
    //  $template
    //  thinkphp_5.0.7_core/public/../application/index/view/test/test.html
    //  $vars
    //  array (size=1)
    //      'name' => string 'hello' (length=5)
    //  $config
    //  array (size=0)
    //   empty
    // +----------------------------------------------------------------------
    if ($vars) {
        $this->data = $vars;
    }
    if ($config) {
        $this->config($config);
    }

    if (!empty($this->config['cache_id']) && $this->config['display_cache']) {
        // 读取渲染缓存
        $cacheContent = Cache::get($this->config['cache_id']);
        if (false !== $cacheContent) {
            echo $cacheContent;
            return;
        }
    }
    $template = $this->parseTemplateFile($template);
    // +----------------------------------------------------------------------
    // |    thinkphp_5.0.7_core/public/../application/index/view/test/test.html
    // +----------------------------------------------------------------------

    if ($template) {
        $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($template) . '.' . ltrim($this->config['cache_suffix'], '.');
        // +----------------------------------------------------------------------
        //  $cacheFile
        //      thinkphp_5.0.7_core/runtime/temp/9495b6f57eb3fe65e44d90b56e260532.php
        // +----------------------------------------------------------------------
        if (!$this->checkCache($cacheFile)) {
            // 缓存无效 重新模板编译
            // +----------------------------------------------------------------------
            // |    读取末班内容
            // +----------------------------------------------------------------------
            $content = file_get_contents($template);
            // +----------------------------------------------------------------------
            // | 编译视图文件 //变量替换标签变量替换php代码  系统定义标签替换字符路径输出
            // +----------------------------------------------------------------------
            $this->compiler($content, $cacheFile);
        }
        // 页面缓存
        ob_start();
        ob_implicit_flush(0);
        // 读取编译存储
        $this->storage->read($cacheFile, $this->data);
        // 获取并清空缓存
        $content = ob_get_clean();
        if (!empty($this->config['cache_id']) && $this->config['display_cache']) {
            // 缓存页面输出
            Cache::set($this->config['cache_id'], $content, $this->config['cache_time']);
        }
        echo $content;
    }
}
  • 编译视图文件 $this->compiler($content, $cacheFile);
$this->compiler($content, $cacheFile);
path think\Template compiler()
    /**
     * 编译模板文件内容
     * @access private
     * @param string    $content 模板内容
     * @param string    $cacheFile 缓存文件名
     * @return void
     */
    private function compiler(&$content, $cacheFile)
    {
        // 判断是否启用布局
        if ($this->config['layout_on']) {
            if (false !== strpos($content, '{__NOLAYOUT__}')) {
                // 可以单独定义不使用布局
                $content = str_replace('{__NOLAYOUT__}', '', $content);
            } else {
                // 读取布局模板
                $layoutFile = $this->parseTemplateFile($this->config['layout_name']);
                if ($layoutFile) {
                    // 替换布局的主体内容
                    $content = str_replace($this->config['layout_item'], $content, file_get_contents($layoutFile));
                }
            }
        } else {
            $content = str_replace('{__NOLAYOUT__}', '', $content);
        }

        //var_dump($content); 解析前
        // +----------------------------------------------------------------------
        //            <!DOCTYPE html>
        //    <html lang="en">
        //    <head>
        //        <meta charset="UTF-8">
        //        <title>Title</title>
        //    </head>
        //    <body>
        //    <hr />
        //    __ROOT__
        //    <h1>{$name}</h1>
        //    </body>
        //    </html>
        // +----------------------------------------------------------------------
        // 模板解析
        $this->parse($content);

        /**
        //var_dump($content); 解析后
        // +----------------------------------------------------------------------
        //    <!DOCTYPE html>
        //    <html lang="en">
        //    <head>
        //        <meta charset="UTF-8">
        //        <title>Title</title>
        //    </head>
        //    <body>
        //    <hr />
        //    __ROOT__
        //    <h1><?php echo $name; ?></h1>
        //    </body>
        //    </html>
        // +----------------------------------------------------------------------
        */


        if ($this->config['strip_space']) {
            /* 去除html空格与换行 */
            $find    = ['~>\s+<~', '~>(\s+\n|\r)~'];
            $replace = ['><', '>'];
            $content = preg_replace($find, $replace, $content);
        }
        // 优化生成的php代码
        $content = preg_replace('/\?>\s*<\?php\s(?!echo\b)/s', '', $content);
        // 模板过滤输出
        $replace = $this->config['tpl_replace_string'];
        $content = str_replace(array_keys($replace), array_values($replace), $content);
        // 添加安全代码及模板引用记录
        $content = '<?php if (!defined(\'THINK_PATH\')) exit(); /*' . serialize($this->includeFile) . '*/ ?>' . "\n" . $content;
        // 编译存储
        $this->storage->write($cacheFile, $content);
        $this->includeFile = [];
        return;
    }
  • 模板解析入口parse
path think\Template parse()
    /**
     * 模板解析入口
     * 支持普通标签和TagLib解析 支持自定义标签库
     * @access public
     * @param string $content 要解析的模板内容
     * @return void
     */
    public function parse(&$content)
    {
        // 内容为空不解析
        if (empty($content)) {
            return;
        }

        // 替换literal标签内容
        // +----------------------------------------------------------------------
        //    literal 原样输出 防止标签被解析
        //      {literal} Hello,{$name}!{/literal}
        // +----------------------------------------------------------------------
        $this->parseLiteral($content);

        // 解析继承
        // +----------------------------------------------------------------------
        // |    {extend name="base" /}
        // +----------------------------------------------------------------------
        $this->parseExtend($content);


        // 解析布局
        // +----------------------------------------------------------------------
        // | {layout name="layout" /} 使用参考地址 http://www.kancloud.cn/manual/thinkphp5/125013
        // +----------------------------------------------------------------------
        $this->parseLayout($content);

        // 检查include语法
        // +----------------------------------------------------------------------
        // |    {include file='模版文件1,模版文件2,...' /}
        // +----------------------------------------------------------------------
        $this->parseInclude($content);

        // 替换包含文件中literal标签内容
        $this->parseLiteral($content);

        // 检查PHP语法
        $this->parsePhp($content);

        // 获取需要引入的标签库列表
        // 标签库只需要定义一次,允许引入多个一次
        // 一般放在文件的最前面
        // 格式:<taglib name="html,mytag..." />
        // 当TAGLIB_LOAD配置为true时才会进行检测
        if ($this->config['taglib_load']) {
            $tagLibs = $this->getIncludeTagLib($content);
            if (!empty($tagLibs)) {
                // 对导入的TagLib进行解析
                foreach ($tagLibs as $tagLibName) {
                    $this->parseTagLib($tagLibName, $content);
                }
            }
        }
        // 预先加载的标签库 无需在每个模板中使用taglib标签加载 但必须使用标签库XML前缀
        if ($this->config['taglib_pre_load']) {
            $tagLibs = explode(',', $this->config['taglib_pre_load']);
            foreach ($tagLibs as $tag) {
                $this->parseTagLib($tag, $content);
            }
        }
        // 内置标签库 无需使用taglib标签导入就可以使用 并且不需使用标签库XML前缀
        $tagLibs = explode(',', $this->config['taglib_build_in']);
        foreach ($tagLibs as $tag) {
            $this->parseTagLib($tag, $content, true);
        }
        // 解析普通模板标签 {$tagName}
        $this->parseTag($content);

        // 还原被替换的Literal标签
        $this->parseLiteral($content, true);
        return;
    }

  • 至此内容模板标签解析完成,接下来返回APP,继续执行send剩余代码,编辑header状态码、头信息->输出内容->执行响应->清空本次请求,整个响应用户操作结束。