完整实现PHP框架(2)-模版引擎的原理以及实现
上一章节,我们讲述了ppf是如何工作的(链接),这一章节,我们主要讲解下ppf中的模版引擎的原理以及实现,我们先来认识下模版引擎是什么
什么是模版引擎
在现代的web编程中,MVC模式已成为主流,为了让前后端更好的分工协作,模版引擎就是作为视图层跟模型层分离的解决办法,所以我们可以从早期的混编过度到现在的模版引擎来实现模版引擎的实现情况
早期的动态页面事这样表示的:
<html>
<head>
<title><?php echo $result;?></title>
</head>
<body>
<?php if ($max == 4) {?>
<?php echo 111;?>
<?php }?>
</body>
</html>
然后我们目标的模版引擎结果应该像这样
<html>
<head>
<title>{$result}</title>
</head>
<body>
{if $max == 4}
111
{/if}
</body>
</html>
模版引擎工作流程:
既然有了目标,那么实现的方法就自然而然得出来了。模版引擎的流程应该是这样:
- 首先通过正则表达式将{$result} 替换成<?php echo $result;?>
- 实现变量的赋值与注入
- 定义好变量后,include这个替换后的php文件,使用ob_start()打开输出缓冲区,捕获文件内容,变量注入,然后将其另存为html页面
- 这个时候的html就是最终的视图页
搭建基础的模版引擎框架
1.我们先来定义一个编译类Compile.php,我们完成以下工作,简单的正则匹配替换,然后传入源文件替换成编译后的文件
<?php
/**
* 编译类 Compile
* 正则匹配式可拓展 可设定多个编译模版
*
*/
class Compile
{
public $config = array(
'compiledir' => 'Cache/', //设置编译后存放的目录
'suffix_cache' => '.htm', //设置编译文件的后缀
);
public $value = array();
public $compare_pattern = array();
public $compare_destpattern = array();
public $compare_include_pattern = "";
public function __CONSTRUCT()
{
//添加include 模版
$this->compare_pattern[] = '#\{include (.*?)\}#';
//简单的key value赋值
$this->compare_pattern[] = '#\{\\$(.*?)\}#';
//if条件语句实现
$this->compare_pattern[] = '#\{if (.*?)\}#';
$this->compare_pattern[] = '#\{elseif(.*?)\}#';
$this->compare_pattern[] = '#\{else(.*?)\}#';
$this->compare_pattern[] = '#\{/if\}#';
//foreach实现
$this->compare_pattern[] = '#\{foreach name=\\$(.*?) item=(.*?) value=(.*?)\}#';
$this->compare_pattern[] = '#\{\\$(.*?)\}#';
$this->compare_pattern[] = '#\{/foreach\}#';
//支持原生php语言实现
$this->compare_pattern[] = '#\{php (.*?)\}#';
$this->compare_pattern[] = '#\{/php\}#';
$this->compare_pattern[] = '#\{compile_include (.*?)\}#';
//以下是上面几个模版编译后的php语言实现
$this->compare_destpattern[] = "<?php include PPF_PATH.'/'.\$this->config['compiledir'].".Dispath::$current_module.".'/'.md5('".Dispath::$current_controller.'_'.Dispath::$current_action.'_'."\\1').'.php'; ?>";
$this->compare_destpattern[] = "<?php echo $\\1;?>";
$this->compare_destpattern[] = "<?php if(\\1){ ?>";
$this->compare_destpattern[] = "<?php }else if(\\1){ ?>";
$this->compare_destpattern[] = "<?php }else{ ?>";
$this->compare_destpattern[] = "<?php }?>";
$this->compare_destpattern[] = "<?php foreach(\$\\1 as \$\\2 => \$\\3){?>";
$this->compare_destpattern[] = '<?php echo $\\1; ?>';
$this->compare_destpattern[] = "<?php } ?>";
$this->compare_destpattern[] = "<?php \\1 ";
$this->compare_destpattern[] = "?>";
$this->compare_destpattern[] = '<?php include "'.APPLICATION_PATH.'/'.Dispath::$current_module.'/View/'.'\\1";?>';
$this->compare_include_pattern = '#\{include (.*?)\}#';
}
/**
* 基本的编译功能实现 讲视图文件通过正则匹配编译并写入到php文件中
*
*/
public function compile($pre_compile_file,$dest_compile_file)
{
$compile_content = preg_replace($this->compare_pattern,$this->compare_destpattern,file_get_contents($pre_compile_file));
file_put_contents($dest_compile_file,$compile_content);
}
}
?>
这个类很简单,但是涵盖了大多数的模版语言(包括include,key-value赋值,if,foreach,原声方法),具体可以参考 ppf手册-模版引擎中的使用方法
这个类中有一个方法compile 需要传递2个变量的,一个是带编译的文件,一个是编译后的文件,所以我们需要创建一个方法来给这个方法传值。
我们可以回到Controller 也就是控制器,我们仿造smarty一样,我们可以写出类似以下的语法
public function addAction() {
$fruit = array("loving"=>'banana',"hating"=>'apple',"no_sense"=>'orange');
$this->view->assign("fruit",$fruit);
$this->view->assign("result","hello");
$this->view->show();
}
以上应该有2个步骤,一个是assign 也就是变量的赋值
第二个也就是show方法,其中实现的逻辑肯定是模版的编译以及变量的注入,再到最后页面的展示过程,ppf中这2个方法是通过$this->view来传递的,所以view这个类必须有这2个方法或者继承父类的方法
view.php
<?php
/**
* 视图类 继承于 Template类
*
*/
class View extends Template
{
}
?>
这里面啥也不写,就是单纯的继承Template类,而这个类才是模版引擎的核心
Template类构筑的思想
基础方法:
- 申明assign,show这2个方法
- 要在构造函数的时候对Compile类的实例化,并调用compile方法完成模版渲染,
高级方法:
- 设置编译策略(目标是判断是否需要编译或者是直接使用缓存文件)
- 给Controller提供是否需要编译的方法(默认是都需要进行编译的)
好的,我们一步步实现这些功能
1.assign方法
/**
* 将变量赋值到$this->vaule中
* @param $key
* @param $value
*/
public function assign($key, $value) {
$this->value[$key] = $value;
}
2.show方法
/**
* 视图跳转方法(包含了模版引擎,模版编译转化功能)
* @param $file 视图跳转文件
*
*/
public function show($file = null) {
/**
* 将例如assign("test","aaa") 转化成 $test = 'aaa';
* 所以这块是有2个赋值情况 一个是$test = 'aaa' 另一个是 $this->value['test'] = 'aaa';
* 这里设定 可以支持多维数组传递赋值
* @param string $file 视图文件
*/
foreach ($this->value as $key => $val) {
$$key = $val;
}
$current_module = Dispath::$current_module;
$current_controller = Dispath::$current_controller;
$compile_file_path = PPF_PATH . '/' . $this->config['compiledir'] . $current_module . '/';
/**
* 如果未指定视图名称则默认跳至该current_action的名称
* 在这块定义视图地址,编译php文件地址,缓存htm文件地址
*/
if (!$file) {
$current_action = Dispath::$current_action;
$html_file = APPLICATION_PATH . '/' . $current_module . '/View/' . $current_controller . '/' . $current_action . '.html';
$compile_file = $compile_file_path . md5($current_controller . '_' . $current_action) . '.php';
$cache_file = $compile_file_path . md5($current_controller . '_' . $current_action) . $this->config['suffix_cache'];
} else {
$html_file = APPLICATION_PATH . '/' . $current_module . '/View/' . $current_controller . '/' . $file . '.html';
$compile_file = $compile_file_path . md5($current_controller . '_' . $file) . '.php';
$cache_file = $compile_file_path . md5($current_controller . '_' . $file) . $this->config['suffix_cache'];
}
/**
* 如果存在视图文件html_file 则继续根据条件编译,否则跳至/Index/view/Notfound/index.html
*/
if (is_file($html_file)) {
/**
* 对compile_file_path进行是否为路径的判断 如果不是 则进行创建并赋予755的权限
*/
if (!is_dir($compile_file_path)) {
mkdir($compile_file_path);
//chmod($compile_file_path, 0755);
}
/**
* 这3行代码是将Controller.php文件某一方法例如:$this->assign("add",'test');
* 将这个以键值对的方式传给在__CONSTRUCT实例化的Compile类中,并通过compile方法进行翻译成php文件
* 最后ob_start()方法需要 include $compile_file;
*/
if ($this->cache_strategy($html_file, $compile_file)) {
$this->compile->value = $this->value;
/**
* @desc 这里是先编译include部分的内容,然后在全部编译完毕
*/
ob_start();
$this->compile->match_include_file($html_file);
$this->compile->compile($html_file, $compile_file);
include "$compile_file";
/**
* 这块是得到输出缓冲区的内容并将其写入缓存文件$cache_file中,同时将编译文件跟缓存文件进行赋予755权限
* 这时可以去看看Cache下面会有2个文件 一个是php文件 一个是htm文件 htm文件就是翻译成html语言的缓存文件
*/
$message = ob_get_contents();
/**
if(file_exists($compile_file)) {
chmod($compile_file, 0777);
}
if(file_exists($cache_file)) {
chmod($cache_file, 0777);
}
*/
$file_line = file_put_contents($cache_file, $message);
ob_end_flush();
} else {
include "$cache_file";
}
} else {
include APPLICATION_PATH . '/Index/View/Notfound/index.html';
}
}
上述的方法中最关键的地方在于如果在视图文件中含有include的方法,例如{include 'Public/header.html'},这是包含公共header.html文件,而往往这些文件也是需要变量赋值的,那也就是说include文件也是需要编译的,所以我们需要实现编译好include文件,然后再编译body体
我们需要在Compile类中还要定义一个方法,用来编译include这个文件
/**
* @desc 这块内容是先将include编译,然后生成在对应cache目录下的php文件,
* 将数组回传给Template.php然后在Template.php进行编译
*
*/
public function match_include_file($file) {
$matchArr = array();
$match_file = preg_match_all($this->compare_include_pattern,file_get_contents($file),$matchArr);
if($match_file && !empty($matchArr[1])) {
$include_file_arr = array();
foreach($matchArr[1] as $key => $val) {
$compile_file_path = $this->get_compile_file_path();
$destpatternFile = APPLICATION_PATH."/".Dispath::$current_module."/View/".$val;
$compile_content = preg_replace($this->compare_pattern,$this->compare_destpattern,file_get_contents($destpatternFile));
$compile_file = $compile_file_path . md5(Dispath::$current_controller . '_' . Dispath::$current_action.'_'.$val) . '.php';
file_put_contents($compile_file,$compile_content);
$include_file_arr[] = $compile_file;
}
return $include_file_arr;
}else {
return false;
}
}
就是正则匹配include模版,然后遍历所有include进来的文件,将其写入缓存文件中,返回这些文件名
3.这里需要使用缓存策略方法cache_strategy()
/**
* 缓存策略
* 根据need_compile 是否需要重新编译
* 以及当前时间比该文件编译时间是否大于自动更新cache_time时间
* 以上2点来决定是需要再次编译还是直接使用缓存文件
* @param string $html_file 视图文件
* @param string $compile_file 编译文件
* @return bool $default_status 是否需要重新编译
*/
public function cache_strategy($html_file, $compile_file) {
$default_status = false;
if(file_exists($compile_file)) {
$compile_file_time = filemtime($compile_file);
}
$time_minus = time() - $compile_file_time;
if (($this->config['need_compile']) || ($time_minus > $this->config['cache_time']) || filemtime($compile_file) < filemtime($html_file)) {
$default_status = true;
} else {
$default_status = false;
}
return $default_status;
}
4.给Controller提供是否需要编译的方法(默认是都需要进行编译的)
/**
* 设置是否缓存(给控制器调用)
*
*/
public function set_compile($value) {
if(is_bool($value)) {
$this->config['need_compile'] = $value;
}else {
throw new FrameException("设置缓存编译参数不正确",0);
}
}
这里需要传递bool值,如果传的不是bool值的话,会抛出框架异常,这个异常是在Controller中定义的.后续会讲到如何申明异常方法
以上过程就是所有的模版编译过程的思路跟实现方式,可以从这里获取到ppf最新文章,大家可以仔细参考 ppf-github 中的Compile.php 以及Template.php这2个文件,
以及查看 ppf手册-模版引擎 的相关内容,一定会有些许收获