tp5源码分析之模板引擎
模板引擎,负责将模板变量填充到模板文件中,
默认模板引擎实现在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