完整实现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手册-模版引擎 的相关内容,一定会有些许收获

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容