牛骨文教育服务平台(让学习变的简单)
博文笔记

tp5源码分析之模板引擎

创建时间:2018-02-23 投稿人: 浏览次数:452

模板引擎,负责将模板变量填充到模板文件中,

默认模板引擎实现在viewThink.php文件中
tp5还支持php文件的模板,其接口与Think模板引擎一致。具体见viewPhp.php文件。

$think->__construct()

模板引擎构造函数,创建模板引擎对象

public function __construct($config = [])
    {
        $this->config = array_merge($this->config, $config);
        if (empty($this->config["view_path"])) {
            $this->config["view_path"] = App::$modulePath . "view" . DS;
        }

        $this->template = new Template($this->config);
    }

分析代码可知,模板引擎的最终实现在Template.php文件中
Think.php文件只是模板引擎的接口封装

$think->config()

模板引擎配置,修改模板引擎配置参数

public function config($name, $value = null)
    {
        if (is_array($name)) {
            $this->template->config($name);
        } else {
            $this->template->$name = $value;
        }
    }

正如上面所说,模板引擎负责将模板文件和模板变量解析为Web页面,因此模板引擎的主要接口就是模板文件的解析操作。

2-1 模板解析接口

$think->fetch()

获取模板文件解析结果

public function fetch($template, $data = [], $config = [])
    {
        if ("" == pathinfo($template, PATHINFO_EXTENSION)) {
            // 获取模板文件名
            $template = $this->parseTemplate($template);
        }
        // 模板不存在 抛出异常
        if (!is_file($template)) {
            throw new TemplateNotFoundException("template not exists:" . $template, $template);
        }
        // 记录视图信息
        App::$debug && Log::record("[ VIEW ] " . $template . " [ " . var_export(array_keys($data), true) . " ]", "info");
        $this->template->fetch($template, $data, $config);
    }

$think->display()

输出模板文件解析结果

public function display($template, $data = [], $config = [])
    {
        $this->template->display($template, $data, $config);
    }

2-2 模板解析辅助

$think->parseTemplate()

定位模板文件

private function parseTemplate($template)
    {
        // 获取视图根目录
        if (strpos($template, "@")) {
            // 跨模块调用
            list($module, $template) = explode("@", $template);
            $path                    = APP_PATH . $module . DS . "view" . DS;
        } else {
            // 当前视图目录
            $path = $this->config["view_path"];
        }

        // 分析模板文件规则
        $request    = Request::instance();
        $controller = Loader::parseName($request->controller());
        if ($controller && 0 !== strpos($template, "/")) {
            $depr     = $this->config["view_depr"];
            $template = str_replace(["/", ":"], $depr, $template);
            if ("" == $template) {
                // 如果模板文件名为空 按照默认规则定位
                $template = str_replace(".", DS, $controller) . $depr . $request->action();
            } elseif (false === strpos($template, $depr)) {
                $template = str_replace(".", DS, $controller) . $depr . $template;
            }
        }
        return $path . ltrim($template, "/") . "." . ltrim($this->config["view_suffix"], ".");
    }

$think->exists()

检查模板文件是否存在

public function exists($template)
    {
        if ("" == pathinfo($template, PATHINFO_EXTENSION)) {
            // 获取模板文件名
            $template = $this->parseTemplate($template);
        }
        return is_file($template);
    }

内置模板引擎类型为配置文件中的Think.其实现在Template.php文件中

3-1 模板引擎

$template->__construct()

创建模板引擎对象

public function __construct(array $config = [])
    {
        $this->config["cache_path"]   = TEMP_PATH;
        $this->config                 = array_merge($this->config, $config);
        $this->config["taglib_begin"] = $this->stripPreg($this->config["taglib_begin"]);
        $this->config["taglib_end"]   = $this->stripPreg($this->config["taglib_end"]);
        $this->config["tpl_begin"]    = $this->stripPreg($this->config["tpl_begin"]);
        $this->config["tpl_end"]      = $this->stripPreg($this->config["tpl_end"]);

        // 初始化模板编译存储器
        $type          = $this->config["compile_type"] ? $this->config["compile_type"] : "File";
        $class         = false !== strpos($type, "\") ? $type : "\think\template\driver\" . ucwords($type);
        $this->storage = new $class();
    }

由上可知,创建模板引擎对象时,可以指定模板编译结果存储方式,默认为File,使用本地文件存储,其实现在templatedriverFile.php文件

$template->config()

获取或修改模板引擎的配置参数

public function config($config)
    {
        if (is_array($config)) {
            $this->config = array_merge($this->config, $config);
        } elseif (isset($this->config[$config])) {
            return $this->config[$config];
        }
    }

3-2 模板引擎变量操作

模板引擎 可以存储将要填充的数据到模板变量中

$template->assign()

存储数据到模板变量中

public function assign($name, $value = "")
    {
        if (is_array($name)) {
            $this->data = array_merge($this->data, $name);
        } else {
            $this->data[$name] = $value;
        }
    }

$template->get()

读取模板变量的数据值

public function get($name = "")
    {
        if ("" == $name) {
            return $this->data;
        } else {
            $data = $this->data;
            foreach (explode(".", $name) as $key => $val) {
                if (isset($data[$val])) {
                    $data = $data[$val];
                } else {
                    $data = null;
                    break;
                }
            }
            return $data;
        }
    }

3-2 模板引擎文件操作

模板引擎,将模板变量填充到模板文件中解析为Web界面。因此模板引擎可以对模板文件进行解析操作。

$template->display()

渲染模板内容,

public function display($content, $vars = [], $config = [])
    {
        if ($vars) {
            $this->data = $vars;
        }
        if ($config) {
            $this->config($config);
        }
        $cacheFile = $this->config["cache_path"] . $this->config["cache_prefix"] . md5($content) . "." . ltrim($this->config["cache_suffix"], ".");
        if (!$this->checkCache($cacheFile)) {
            // 缓存无效 模板编译
            $this->compiler($content, $cacheFile);
        }
        // 读取编译存储
        $this->storage->read($cacheFile, $this->data);
    }

$template->fetch()

渲染模板文件

public function fetch($template, $vars = [], $config = [])
    {
        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);
        if ($template) {
            $cacheFile = $this->config["cache_path"] . $this->config["cache_prefix"] . md5($template) . "." . ltrim($this->config["cache_suffix"], ".");
            if (!$this->checkCache($cacheFile)) {
                // 缓存无效 重新模板编译
                $content = file_get_contents($template);
                $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;
        }
    }

3-3 模板引擎编译缓存

模板引擎解析模板文件中根据配置信息,可以进行编译缓存和渲染缓存

编译缓存(tpl_cahce),使用编译存储器(上面构造函数中创建的)存储
渲染缓存(display_cache),使用缓冲机制(Cache.php)存储

$template->checkCache()

检查模板编译缓存是否有效(tpl_cache)

private function checkCache($cacheFile)
    {
        // 未开启缓存功能
        if (!$this->config["tpl_cache"]) {
            return false;
        }
        // 缓存文件不存在
        if (!is_file($cacheFile)) {
            return false;
        }
        // 读取缓存文件失败
        if (!$handle = @fopen($cacheFile, "r")) {
            return false;
        }
        // 读取第一行
        preg_match("//*(.+?)*//", fgets($handle), $matches);
        if (!isset($matches[1])) {
            return false;
        }
        $includeFile = unserialize($matches[1]);
        if (!is_array($includeFile)) {
            return false;
        }
        // 检查模板文件是否有更新
        foreach ($includeFile as $path => $time) {
            if (is_file($path) && filemtime($path) > $time) {
                // 模板文件如果有更新则缓存需要更新
                return false;
            }
        }
        // 检查编译存储是否有效
        return $this->storage->check($cacheFile, $this->config["cache_time"]);
    }

$template->isCache()

检查渲染缓存(display_cache)是否存在

public function isCache($cacheId)
    {
        if ($cacheId && $this->config["display_cache"]) {
            // 缓存页面输出
            return Cache::has($cacheId);
        }
        return false;
    }

4-1 模板解析文件级操作

模板引擎解析的辅助函数,在fetch/display接口中调用,用来解析模板文件的内容。

模板引擎解析模板文件时首先读取模板文件内容
然后配合模板变量解析模板文件。

$template->parseTemplate()

读取模板文件内容

private function parseTemplateFile($template)
    {
        if ("" == pathinfo($template, PATHINFO_EXTENSION)) {
            if (strpos($template, "@")) {
                // 跨模块调用模板
                $template = str_replace(["/", ":"], $this->config["view_depr"], $template);
                $template = APP_PATH . str_replace("@", "/" . basename($this->config["view_path"]) . "/", $template);
            } else {
                $template = str_replace(["/", ":"], $this->config["view_depr"], $template);
                $template = $this->config["view_path"] . $template;
            }
            $template .= "." . ltrim($this->config["view_suffix"], ".");
        }

        if (is_file($template)) {
            // 记录模板文件的更新时间
            $this->includeFile[$template] = filemtime($template);
            return $template;
        } else {
            throw new TemplateNotFoundException("template not exists:" . $template, $template);
        }
    }

$template->compiler()

编译模板文件

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);
        }

        // 模板解析
        $this->parse($content);
        if ($this->config["strip_space"]) {
            /* 去除html空格与换行 */
            $find    = ["~>s+<~", "~>(s+
|
)~"];
            $replace = ["><", ">"];
            $content = preg_replace($find, $replace, $content);
        }
        // 优化生成的php代码
        $content = preg_replace("/?>s*<?phps(?!echo)/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) . "*/ ?>" . "
" . $content;
        // 编译存储
        $this->storage->write($cacheFile, $content);
        $this->includeFile = [];
        return;
    }

上面的模板编译过程中

首先 检查是否启用布局,读取布局文件到模板文件中
然后 解析读取的模板文件内容。
最后 优化模板编译结果并保存

$template->parse()

解析模板内容

public function parse(&$content)
    {
        // 内容为空不解析
        if (empty($content)) {
            return;
        }
        // 替换literal标签内容
        $this->parseLiteral($content);
        // 解析继承
        $this->parseExtend($content);
        // 解析布局
        $this->parseLayout($content);
        // 检查include语法
        $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;
    }

上面的模板解析过程中

首先 依次解析模板文件中出现的literal,extend,layout,include,php等特殊语法字符串。
然后 调用标签库再次对其中的自定义标签进行解析
最后 解析普通模板变量标签

这些模板内容的解析过程称为语法块级解析

4-2模板解析语法块级解析

$template->parseLiteral()

literal语法解析

private function parseLiteral(&$content, $restore = false)
    {
        $regex = $this->getRegex($restore ? "restoreliteral" : "literal");
        if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
            if (!$restore) {
                $count = count($this->literal);
                // 替换literal标签
                foreach ($matches as $match) {
                    $this->literal[] = substr($match[0], strlen($match[1]), -strlen($match[2]));
                    $content         = str_replace($match[0], "<!--###literal{$count}###-->", $content);
                    $count++;
                }
            } else {
                // 还原literal标签
                foreach ($matches as $match) {
                    $content = str_replace($match[0], $this->literal[$match[1]], $content);
                }
                // 清空literal记录
                $this->literal = [];
            }
            unset($matches);
        }
        return;
    }

$template->parseExtend()

extend语法解析

private function parseExtend(&$content)
    {
        $regex  = $this->getRegex("extend");
        $array  = $blocks  = $baseBlocks  = [];
        $extend = "";
        $func   = function ($template) use (&$func, &$regex, &$array, &$extend, &$blocks, &$baseBlocks) {
            if (preg_match($regex, $template, $matches)) {
                if (!isset($array[$matches["name"]])) {
                    $array[$matches["name"]] = 1;
                    // 读取继承模板
                    $extend = $this->parseTemplateName($matches["name"]);
                    // 递归检查继承
                    $func($extend);
                    // 取得block标签内容
                    $blocks = array_merge($blocks, $this->parseBlock($template));
                    return;
                }
            } else {
                // 取得顶层模板block标签内容
                $baseBlocks = $this->parseBlock($template, true);
                if (empty($extend)) {
                    // 无extend标签但有block标签的情况
                    $extend = $template;
                }
            }
        };

        $func($content);
        if (!empty($extend)) {
            if ($baseBlocks) {
                $children = [];
                foreach ($baseBlocks as $name => $val) {
                    $replace = $val["content"];
                    if (!empty($children[$name])) {
                        // 如果包含有子block标签
                        foreach ($children[$name] as $key) {
                            $replace = str_replace($baseBlocks[$key]["begin"] . $baseBlocks[$key]["content"] . $baseBlocks[$key]["end"], $blocks[$key]["content"], $replace);
                        }
                    }
                    if (isset($blocks[$name])) {
                        // 带有{__block__}表示与所继承模板的相应标签合并,而不是覆盖
                        $replace = str_replace(["{__BLOCK__}", "{__block__}"], $replace, $blocks[$name]["content"]);
                        if (!empty($val["parent"])) {
                            // 如果不是最顶层的block标签
                            $parent = $val["parent"];
                            if (isset($blocks[$parent])) {
                                $blocks[$parent]["content"] = str_replace($blocks[$name]["begin"] . $blocks[$name]["content"] . $blocks[$name]["end"], $replace, $blocks[$parent]["content"]);
                            }
                            $blocks[$name]["content"] = $replace;
                            $children[$parent][]      = $name;
                            continue;
                        }
                    } elseif (!empty($val["parent"])) {
                        // 如果子标签没有被继承则用原值
                        $children[$val["parent"]][] = $name;
                        $blocks[$name]              = $val;
                    }
                    if (!$val["parent"]) {
                        // 替换模板中的顶级block标签
                        $extend = str_replace($val["begin"] . $val["content"] . $val["end"], $replace, $extend);
                    }
                }
            }
            $content = $extend;
            unset($blocks, $baseBlocks);
        }
        return;
    }

$template->parseLayout()

layout语法解析

private function parseInclude(&$content)
    {
        $regex = $this->getRegex("include");
        $func  = function ($template) use (&$func, &$regex, &$content) {
            if (preg_match_all($regex, $template, $matches, PREG_SET_ORDER)) {
                foreach ($matches as $match) {
                    $array = $this->parseAttr($match[0]);
                    $file  = $array["file"];
                    unset($array["file"]);
                    // 分析模板文件名并读取内容
                    $parseStr = $this->parseTemplateName($file);
                    foreach ($array as $k => $v) {
                        // 以$开头字符串转换成模板变量
                        if (0 === strpos($v, "$")) {
                            $v = $this->get(substr($v, 1));
                        }
                        $parseStr = str_replace("[" . $k . "]", $v, $parseStr);
                    }
                    $content = str_replace($match[0], $parseStr, $content);
                    // 再次对包含文件进行模板分析
                    $func($parseStr);
                }
                unset($matches);
            }
        };
        // 替换模板中的include标签
        $func($content);
        return;
    }

$template->parseInclude()

include语法解析

private function parseInclude(&$content)
    {
        $regex = $this->getRegex("include");
        $func  = function ($template) use (&$func, &$regex, &$content) {
            if (preg_match_all($regex, $template, $matches, PREG_SET_ORDER)) {
                foreach ($matches as $match) {
                    $array = $this->parseAttr($match[0]);
                    $file  = $array["file"];
                    unset($array["file"]);
                    // 分析模板文件名并读取内容
                    $parseStr = $this->parseTemplateName($file);
                    foreach ($array as $k => $v) {
                        // 以$开头字符串转换成模板变量
                        if (0 === strpos($v, "$")) {
                            $v = $this->get(substr($v, 1));
                        }
                        $parseStr = str_replace("[" . $k . "]", $v, $parseStr);
                    }
                    $content = str_replace($match[0], $parseStr, $content);
                    // 再次对包含文件进行模板分析
                    $func($parseStr);
                }
                unset($matches);
            }
        };
        // 替换模板中的include标签
        $func($content);
        return;
    }

$template->parsePhp()

php语法解析

private function parsePhp(&$content)
    {
        // 短标签的情况要将<?标签用echo方式输出 否则无法正常输出xml标识
        $content = preg_replace("/(<?(?!php|=|$))/i", "<?php echo "\1"; ?>" . "
", $content);
        // PHP语法检查
        if ($this->config["tpl_deny_php"] && false !== strpos($content, "<?php")) {
            throw new Exception("not allow php tag", 11600);
        }
        return;
    }

$template->parseTagLib()

标签库解析,使用标签库解析模板内容,标签库的使用 见 模板标签库 章节

public function parseTagLib($tagLib, &$content, $hide = false)
    {
        if (false !== strpos($tagLib, "\")) {
            // 支持指定标签库的命名空间
            $className = $tagLib;
            $tagLib    = substr($tagLib, strrpos($tagLib, "\") + 1);
        } else {
            $className = "\think\template\taglib\" . ucwords($tagLib);
        }
        $tLib = new $className($this);
        $tLib->parseTag($content, $hide ? "" : $tagLib);
        return;
    }

$template->parseTag()

简单模板标签解析

private function parseTag(&$content)
    {
        $regex = $this->getRegex("tag");
        if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
            foreach ($matches as $match) {
                $str  = stripslashes($match[1]);
                $flag = substr($str, 0, 1);
                switch ($flag) {
                    case "$":
                        // 解析模板变量 格式 {$varName}
                        // 是否带有?号
                        if (false !== $pos = strpos($str, "?")) {
                            $array = preg_split("/([!=]={1,2}|(?<!-)[><]={0,1})/", substr($str, 0, $pos), 2, PREG_SPLIT_DELIM_CAPTURE);
                            $name  = $array[0];
                            $this->parseVar($name);
                            $this->parseVarFunction($name);

                            $str = trim(substr($str, $pos + 1));
                            $this->parseVar($str);
                            $first = substr($str, 0, 1);
                            if (strpos($name, ")")) {
                                // $nam
声明:该文观点仅代表作者本人,牛骨文系教育信息发布平台,牛骨文仅提供信息存储空间服务。