PHP模板引擎的实现原理
在原始阶段,一个PHP文件中,PHP代码与HTML混合在一起,程序逻辑、数据存取代码都出现在“页面”中。前端编写页面,也就是用户能够看见的部分,而后端做的就是实现这些逻辑,到这里问题来了,页面到底是前端来渲染还是后端来渲染呢?结果无非就是两种情况,一种是不怎么会后端的前端来写后端代码,一种是不怎么会前端的后端拿着前端页面来做渲染。
上面提到的工作模式最终结果很可能是这样:前端写“烂”后端代码、后端搞坏前端页面,因为既熟悉前端又熟悉后端的人员少之又少。模板引擎的诞生,一个原因是为了提高程序的可维护性,另一个原因就是为了打破这种“工作在知识盲区”的情况,模板标签是模板引擎的重要组成部分,模板标签不是后端代码,大部分模板引擎中的模板标签都是仿照HTML标签而制定的,以方便前端学习和使用。当下流行的PHP模板引擎数不胜数,如Twig、Smarty等等几十种,但是不管哪一种,最基础的原理都是字符替换,将模板标签替换为可执行的PHP代码。
现在前后端分离项目越来越多,模板引擎的重要性越来越低,但在一般展示型站点,模板引擎尚有一席之地。大概两年前,写了一个简单的模板引擎,开始标签与结束标签都是直接硬替换,也就是说在成对出现的标签中,开始标签与结束标签虽然要成对书写,但其实标签之间并没有任何联系。这样做有一定的局限性,比如根据标签中的属性值在结束时想做一些操作就无法实现。于是决定重写,让开始、结束标签之间能够有一定联系。
假定现在有模板文件index.html,内容如下
<tag>
测试
<tag>测试</tag>
测试
</tag>
在思考一番之后,有以下几个问号
- 如何找到对应关系?
- 如果根据标签偏移量替换,怎么保证替换完成后偏移量不发生变化?
思考无果,于是去看了TP的实现,经过一番翻阅源代码、调试,最后将TP的思路大致总结如下
同时匹配开始标签和结束标签,再使用preg_match_all函数合适的第四个参数(使用PREG_SET_ORDER参数将结果重新组合,使用PREG_OFFSET_CAPTURE参数找出了标签在整个文档中的偏移量)找到所有标签,再通过循环判断,如果是开始标签则先入栈,如果是结束标签则把当前结束标签之前的最后一个开始标签作为其对应的开始标签,以建立对应关系。再从后往前替换标签,保证当前标签背替换后,前面的所有标签偏移量不变。因为从后往前替换,所以最先是替换结束标签的,替换结束标签时,检查当前替换的标签之后是否存在开始标签,如果存在则替换掉(如果此处不替换掉开始标签,偏移量就发生了变化),不存在则直接替换掉。最后替换掉所有无结束标签穿插的开始标签。
有兴趣的朋友可以去看看TP5的实现。既然原理知道了,接下来就是动手了。
查找标签
$content = file_get_contents('index.html'); // 标签名 $tag = 'tag'; // 拼接正则 $pattern = '/<(?:(' . $tag . ')\b(?>[^>]*)|\/(' . $tag . '))>/is'; preg_match_all($pattern, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
建立对应关系
// 存放开始标签 $start = []; // 所有节点 $nodes = []; foreach ($matches as $key => $match) { // 如果是结束标签,建立对应关系 if ($match[1][0] == '') { // 以结束位置偏移量为键名,方便排序 // 将当前找到的最后一个开始标签作为当前标签的开始标签 $nodes[$match[0][1]] = [ 'begin' => array_pop($start), 'end' => $match[0], ]; } else { $start[] = $match[0]; } } // 思路是从后往前替换,所以根据结束标签偏移量倒序排列 krsort($nodes);
替换结束标签
// 开始标签位置信息 $beginTags = []; // 替换结束标签 foreach ($nodes as $pos => $node) { // 固定值或根据标签属性去换取具体替换的值,此处做演示 $replace = ['<?php if(true): ?>', '<?php endif; ?>']; while ($beginTags) { $beginTag = end($beginTags); // 检查是否有开始标签在当前结束标签之后,如果存在则先替换掉,以免位置错乱 if ($pos > $beginTag['begin'][1]) { break; } else { $content = substr_replace($content, $replace[0], $beginTag['begin'][1], strlen($beginTag['begin'][0])); // 删除已经替换的开始标签 array_pop($beginTags); } } // 替换结束标签 $content = substr_replace($content, $replace[1], $node['end'][1], strlen($node['end'][0])); 将在此处没有被替换的开始标签装入数组,替换完结束标签后统一进行替换 $beginTags[] = [ 'begin' => $node['begin'], 'end' => $node['end'], ]; }
替换开始标签
// 替换开始标签(没有结束标签穿插的纯开始标签,同样从后往前替换) while ($beginTags) { // 取出最后一个开始标签准备替换 $beginTag = array_pop($beginTags); $content = substr_replace($content, $replace[0], $beginTag['begin'][1], strlen($beginTag['begin'][0])); }
打印$content,如下
<?php if(true): ?> 测试 <?php if(true): ?>测试<?php endif; ?> 测试 <?php endif; ?>
至此,标签的解析就完成了,这也是一个模板引擎的重要组成部分,自闭合标签就不说了,因为更简单。代码是七月份写的,现在才想起来记录一下,于是有了这篇文章。