完整實現(xiàn)PHP框架(2)-模版引擎的原理以及實現(xiàn)

上一章節(jié),我們講述了ppf是如何工作的(鏈接),這一章節(jié),我們主要講解下ppf中的模版引擎的原理以及實現(xiàn),我們先來認(rèn)識下模版引擎是什么

什么是模版引擎

在現(xiàn)代的web編程中,MVC模式已成為主流,為了讓前后端更好的分工協(xié)作,模版引擎就是作為視圖層跟模型層分離的解決辦法,所以我們可以從早期的混編過度到現(xiàn)在的模版引擎來實現(xiàn)模版引擎的實現(xiàn)情況

早期的動態(tài)頁面事這樣表示的:

<html>
<head>
<title><?php echo $result;?></title>
</head>
<body>
<?php if ($max == 4) {?>
<?php echo 111;?>
<?php }?>
</body>
</html>

然后我們目標(biāo)的模版引擎結(jié)果應(yīng)該像這樣

<html>
<head>
<title>{$result}</title>
</head>
<body>
{if $max == 4}
111
{/if}
</body>
</html>

模版引擎工作流程:

既然有了目標(biāo),那么實現(xiàn)的方法就自然而然得出來了。模版引擎的流程應(yīng)該是這樣:

  • 首先通過正則表達(dá)式將{$result} 替換成<?php echo $result;?>
  • 實現(xiàn)變量的賦值與注入
  • 定義好變量后,include這個替換后的php文件,使用ob_start()打開輸出緩沖區(qū),捕獲文件內(nèi)容,變量注入,然后將其另存為html頁面
  • 這個時候的html就是最終的視圖頁

搭建基礎(chǔ)的模版引擎框架

1.我們先來定義一個編譯類Compile.php,我們完成以下工作,簡單的正則匹配替換,然后傳入源文件替換成編譯后的文件

<?php
/**
 *  編譯類  Compile
 *  正則匹配式可拓展  可設(shè)定多個編譯模版
 *
 */
class Compile
{
    public $config = array(
        'compiledir' => 'Cache/',         //設(shè)置編譯后存放的目錄
        'suffix_cache' => '.htm',         //設(shè)置編譯文件的后綴
    );
    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條件語句實現(xiàn)
        $this->compare_pattern[] = '#\{if (.*?)\}#';
        $this->compare_pattern[] = '#\{elseif(.*?)\}#';
        $this->compare_pattern[] = '#\{else(.*?)\}#';
        $this->compare_pattern[] = '#\{/if\}#';
        //foreach實現(xiàn)
        $this->compare_pattern[] = '#\{foreach name=\\$(.*?) item=(.*?) value=(.*?)\}#';
        $this->compare_pattern[] = '#\{\\$(.*?)\}#';
        $this->compare_pattern[] = '#\{/foreach\}#';
        //支持原生php語言實現(xiàn)
        $this->compare_pattern[] = '#\{php (.*?)\}#';
        $this->compare_pattern[] = '#\{/php\}#';

        $this->compare_pattern[] = '#\{compile_include (.*?)\}#';


        //以下是上面幾個模版編譯后的php語言實現(xiàn)

        $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 (.*?)\}#';
    }
    /**
     *   基本的編譯功能實現(xiàn) 講視圖文件通過正則匹配編譯并寫入到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);
    }
}
?>

這個類很簡單,但是涵蓋了大多數(shù)的模版語言(包括include,key-value賦值,if,foreach,原聲方法),具體可以參考 ppf手冊-模版引擎中的使用方法

這個類中有一個方法compile 需要傳遞2個變量的,一個是帶編譯的文件,一個是編譯后的文件,所以我們需要創(chuàng)建一個方法來給這個方法傳值。

我們可以回到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();
}

以上應(yīng)該有2個步驟,一個是assign 也就是變量的賦值

第二個也就是show方法,其中實現(xiàn)的邏輯肯定是模版的編譯以及變量的注入,再到最后頁面的展示過程,ppf中這2個方法是通過$this->view來傳遞的,所以view這個類必須有這2個方法或者繼承父類的方法

view.php

<?php
/**
 *  視圖類 繼承于 Template類
 *
 */
class View extends Template
{
    
}
?>

這里面啥也不寫,就是單純的繼承Template類,而這個類才是模版引擎的核心


Template類構(gòu)筑的思想

基礎(chǔ)方法:

  • 申明assign,show這2個方法
  • 要在構(gòu)造函數(shù)的時候?qū)ompile類的實例化,并調(diào)用compile方法完成模版渲染,

高級方法:

  • 設(shè)置編譯策略(目標(biāo)是判斷是否需要編譯或者是直接使用緩存文件)
  • 給Controller提供是否需要編譯的方法(默認(rèn)是都需要進(jìn)行編譯的)

好的,我們一步步實現(xiàn)這些功能

1.assign方法

 /**
 *  將變量賦值到$this->vaule中
 *  @param $key
 *  @param $value
 */
public function assign($key, $value) {
    $this->value[$key] = $value;
}

2.show方法

/**
 *   視圖跳轉(zhuǎn)方法(包含了模版引擎,模版編譯轉(zhuǎn)化功能)
 *   @param  $file  視圖跳轉(zhuǎn)文件
 *
 */
public function show($file = null) {
    /**
     *  將例如assign("test","aaa") 轉(zhuǎn)化成 $test = 'aaa';
     *  所以這塊是有2個賦值情況  一個是$test = 'aaa' 另一個是 $this->value['test'] = 'aaa';
     *  這里設(shè)定 可以支持多維數(shù)組傳遞賦值
     *  @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 . '/';
    /**
     *   如果未指定視圖名稱則默認(rèn)跳至該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  則繼續(xù)根據(jù)條件編譯,否則跳至/Index/view/Notfound/index.html
     */
    if (is_file($html_file)) {
        /**
         *   對compile_file_path進(jìn)行是否為路徑的判斷 如果不是 則進(jìn)行創(chuàng)建并賦予755的權(quán)限
         */
        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方法進(jìn)行翻譯成php文件
         *   最后ob_start()方法需要  include $compile_file;
         */
        if ($this->cache_strategy($html_file, $compile_file)) {
            $this->compile->value = $this->value;

            /**
             * @desc 這里是先編譯include部分的內(nèi)容,然后在全部編譯完畢
             */
            ob_start();
            $this->compile->match_include_file($html_file);
            $this->compile->compile($html_file, $compile_file);
            include "$compile_file";
            /**
             *   這塊是得到輸出緩沖區(qū)的內(nèi)容并將其寫入緩存文件$cache_file中,同時將編譯文件跟緩存文件進(jìn)行賦予755權(quán)限
             *   這時可以去看看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';
    }
}

上述的方法中最關(guān)鍵的地方在于如果在視圖文件中含有include的方法,例如{include 'Public/header.html'},這是包含公共header.html文件,而往往這些文件也是需要變量賦值的,那也就是說include文件也是需要編譯的,所以我們需要實現(xiàn)編譯好include文件,然后再編譯body體

我們需要在Compile類中還要定義一個方法,用來編譯include這個文件

/**
 * @desc 這塊內(nèi)容是先將include編譯,然后生成在對應(yīng)cache目錄下的php文件,
 * 將數(shù)組回傳給Template.php然后在Template.php進(jìn)行編譯
 *
 */
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進(jìn)來的文件,將其寫入緩存文件中,返回這些文件名

3.這里需要使用緩存策略方法cache_strategy()

/**
 *   緩存策略
 *   根據(jù)need_compile 是否需要重新編譯
 *   以及當(dāng)前時間比該文件編譯時間是否大于自動更新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提供是否需要編譯的方法(默認(rèn)是都需要進(jìn)行編譯的)

/**
 *   設(shè)置是否緩存(給控制器調(diào)用)
 *
 */
public function set_compile($value) {
    if(is_bool($value)) {
        $this->config['need_compile'] = $value;
    }else {
        throw new FrameException("設(shè)置緩存編譯參數(shù)不正確",0);
    }
}

這里需要傳遞bool值,如果傳的不是bool值的話,會拋出框架異常,這個異常是在Controller中定義的.后續(xù)會講到如何申明異常方法

以上過程就是所有的模版編譯過程的思路跟實現(xiàn)方式,可以從這里獲取到ppf最新文章,大家可以仔細(xì)參考 ppf-github 中的Compile.php 以及Template.php這2個文件,

以及查看 ppf手冊-模版引擎 的相關(guān)內(nèi)容,一定會有些許收獲

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容