PHP早期開發中通常是PHP代碼和HTML代碼混寫,這也使代碼中充斥着數據庫操作,邏輯處理等。當項目不大時,這樣的代碼還可以接受,但是隨着項目不斷擴大,我們就會發現同一個文件中同時存在前端邏輯和后端處理,當邏輯越來越復雜時,代碼的可讀性和可維護性都會變得非常差,以至於后來不得不進行大規模的代碼重構。所以后來就出現了代碼分層的思想,盡量拆分開前端代碼和后端代碼。
PHP模板引擎能解決這種混亂嗎?當然可以。但是呢,即使你不用專門的模板引擎也可以寫出邏輯清晰的代碼,重點是要有分層的思想,有專門的腳本去進行數據獲取,數據處理,邏輯處理等,在展示頁面只進行盡可能簡單的邏輯處理即可。既然這樣,那還有使用PHP模板引擎的必要嗎?毫無疑問當然有,因為模板引擎的功能不僅於此。
那接下來就說一下PHP模板引擎的主要作用:
1、它實現了一些自定義標簽,用於展示層的簡單邏輯處理,相較於不適用引擎的好處是代碼看起來不像是PHP代碼了,感覺上HTML代碼和PHP代碼完全分開了,但這只是假象,壞處是效率降低了,因為這樣的頁面需要專門的腳本解析后才能正確顯示,解析的方法就是使用正則表達式替換,這明顯降低了效率。到現在來看感覺PHP模板引擎有還不如沒有呢,那為什么還要用它呢,重點是他的下一個功能。
2、文件緩存,這是模板引擎在生產環境中提高效率的非常好的手段。可以用在頁面加載時所用數據量很大但不經常變或者不需要實時更新,即使延遲一會也無妨的頁面。我個人感覺文件緩存是PHP模板引擎的最重要的部分。
接下來我們就寫一個簡易的模板引擎(最后有完整文件代碼)
首先我們先要計划好我們的所需要的基礎類,有Template類和Compile類。
在我們具體實現編譯功能之前先搭好一個空的骨架,具體如下:
<?php /** * 模板引擎基礎類 */ class Template { private $config = array( 'suffix' => '.m', // 設置模板文件的后綴 'templateDir' => 'template/', // 設置模板所在的文件夾 'compileDir' => 'cache/', // 設置編譯后存放的目錄 'cache_html' => true, // 是否需要編譯成靜態的HTML文件 'suffix_cache' => '.html', // 設置編譯文件的后綴 'cache_time' => 7200, // 多長時間自動更新,單位秒 'php_turn' => true, // 是否支持原生PHP代碼 'cache_control' => 'control.dat', 'debug' => false, ); private static $instance = null; private $value = array(); // 值棧 private $compileTool; // 編譯器 public $file; // 模板文件名,不帶路徑 public $debug = array(); // 調試信息 private $controlData = array(); public function __construct($config = array()) { $this->debug['begin'] = microtime(true); $this->config = $config + $this->config; if (! is_dir($this->config['templateDir'])) { exit("模板目錄不存在!"); } if (! is_dir($this->config['compileDir'])) { mkdir($this->config['compileDir'], 0770); } $this->getPath(); include './Compile.php'; } /** *獲取絕對路徑 */ public function getPath() { $this->config['templateDir'] = strtr(realpath($this->config['templateDir']), '\\', '/').'/'; $this->config['compileDir'] = strtr(realpath($this->config['compileDir']), '\\', '/').'/'; } /** *取得模板引擎的實例 */ public static function getInstance() { if (is_null(self::$instance)) { self::$instance = new self(); } return self::$instance; } /* 設置模板引擎參數 */ public function setConfig($key, $value = null) { if (is_array($key)) { $this->config = $key + $this->config; }else { $this->config[$key] = $value; } } /* 獲取當前模板引擎配置,僅供調試使用 */ public function getConfig($key = null) { if ($key) { return $this->config[$key]; }else { return $this->config; } } /** *注入單個變量 */ public function assign($key, $value) { $this->value[$key] = $value; } /** *注入數組變量 */ public function assignArray($array) { if (is_array($array)) { foreach($array as $k => $v) { $this->value[$k] = $v; } } } /** * 獲取模板文件完整路徑 */ public function path() { return $this->config['templateDir'].$this->file.$this->config['suffix']; } /** *判斷是否開啟了緩存 */ public function needCache() { return $this->config['cache_html']; } /** *顯示模板 */ public function show($file) { } } ?>
從上邊的代碼中我們能發現對於模板文件不存在和編譯文件不存在處理方式不同,這也很容易理解,如果你連模板文件都沒有有啥好編譯的,但是你有模板文件沒編譯文件這也很正常,正常進行編譯即可。
如上所示,我們首先想好了這個模板引擎需要什么配置,還有一些設置配置的方法和檢查配置的方法等,而我們的核心方法show()還沒有實現呢,先不着急,我們先去寫編譯類Compile,如下所示:
<?php class Compile { private $template; // 待編譯的文件 private $content; // 需要替換的文件 private $comfile; // 編譯后的文件 private $left = '{'; // 左定界符 private $right = '}'; // 右定界符 private $value = array(); // 值棧 private $phpTurn; private $T_P = array(); // 匹配正則數組 private $T_R = array(); // 替換數組 public function __construct($template, $compileFile, $config) { $this->template = $template; $this->comfile = $compileFile; $this->content = file_get_contents($template); } public function compile() { $this->c_var(); file_put_contents($this->comfile, $this->content); } public function c_var() { $this->content = preg_replace($this->T_P, $this->T_R, $this->content); } public function __set($name, $value) { $this->$name = $value; } public function __get($name) { return $this->$name; } } ?>
從上面Compile類的構造函數我們可以看出,他需要模板文件路徑,編譯文件路徑,和具體編譯時的配置參數,但是在這這個配置參數嗎,沒有用到。之前說過模板引擎主要使用的正則表達式來進行匹配替換,將模板文件編譯成能正確執行的PHP文件,這里使用數組存放正則匹配數組和替換數組來進行整體替換。
接下來我們就簡單實現幾個常用的標簽,先看怎么替換簡單變量:
// \x7f-\xff表示ASCII字符從127到255,其中\為轉義,作用是匹配漢字 $this->T_P[] = "#\{\\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\}#"; $this->T_R[] = "<?php echo \$this->value['\\1']; ?>";
正如我們看到的,上邊的那個是正則匹配,下邊的是替換。但是我們沒有給編譯類的value變量賦值,那這個替換能找到正確的值嗎?答案是能,因為他用的不是這個類的value用的是模板類的value,接下來一會會講到。
然后我們在看看怎么實現foreach標簽,這個很常用
$this->T_P[] = "#\{(loop|foreach)\s+\\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\s*\}#i"; $this->T_P[] = "#\{\/(loop|foreach)\}#";
$this->T_R[] = "<?php foreach ((array)\$this->value['\\2'] as \$k => \$v) { ?>"; $this->T_R[] = "<?php } ?>";
這里用到的主要正則表達式的知識有:元組、反向引用等,這些只要稍微看一下正則表達式基礎就能理解了。
我們再來一個if else標簽:
$this->T_P[] = "#\{\/(loop|foreach|if)\}#"; $this->T_P[] = "#\{if (.*?)\}#"; $this->T_P[] = "#\{(else if|elseif) (.*?)\}#"; $this->T_P[] = "#\{else\}#"; $this->T_R[] = "<?php } ?>"; $this->T_R[] = "<?php if(\\1){ ?>"; $this->T_R[] = "<?php }elseif(\\2){ ?>"; $this->T_R[] = "<?php }else{ ?>";
我們將if的閉合標簽和foreach的閉合標簽放一塊了。
現在我們已經能編譯一些標簽了我們就再轉回模板類,現在我們想一想要怎么展示呢,這個才是我們的根本目的。寫代碼之前先理一下思路:
1、判斷是否開啟了緩存,如果是進行第二步,否則直接進行編譯輸出。
2、判斷是否需要更新緩存(判斷方式主要是緩存時間和編譯文件和模板文件的修改時間的關系),如果是就進行第三步,否則直接讀取緩存文件輸出。
3、重新編譯模板文件,並將編譯后的PHP文件輸出保存到靜態緩存文件中。
簡單來說就是上邊的那三個步驟,具體實現如下:
/** *是否需要重新生成靜態文件 */ public function reCache($file) { $flag = true; $cacheFile = $this->config['compileDir'].md5($file).$this->config['suffix_cache']; if ($this->config['cache_html'] === true) { $timeFlag = (time() - @filemtime($cacheFile)) < $this->config['cache_time'] ? true : false; if (is_file($cacheFile) && filesize($cacheFile) > 1 && $timeFlag && filemtime($compileFile) >= filemtime($this->path())) { $flag = false; }else { $flag = true; } } return $flag; } /** *顯示模板 */ public function show($file) { $this->file = $file; if (! is_file($this->path())) { exit('找不到對應的模板!'); } $compileFile = $this->config['compileDir'].md5($file).'.php'; $cacheFile = $this->config['compileDir'].md5($file).$this->config['suffix_cache']; extract($this->value, EXTR_OVERWRITE); if ($this->config['cache_html'] === true) { if ($this->reCache($file) === true) { $this->debug['cached'] = 'false'; $this->compileTool = new Compile($this->path(), $compileFile, $this->config); if ($this->needCache()) {ob_start();} if (! is_file($compileFile) || filemtime($compileFile) < filemtime($this->path())) { $this->compileTool->value = $this->value; $this->compileTool->compile(); include $compileFile; }else { include $compileFile; } if ($this->needCache()) { $message = ob_get_contents(); file_put_contents($cacheFile, $message); } }else { readfile($cacheFile); $this->debug['cached'] = 'true'; } }else { if (! is_file($compileFile) || filemtime($compileFile) < filemtime($this->path())) { $this->compileTool = new Compile($this->path(), $compileFile, $this->config); $this->compileTool->value = $this->value; $this->compileTool->compile(); include $compileFile; }else { include $compileFile; } } $this->debug['spend'] = microtime(true) - $this->debug['begin']; $this->debug['count'] = count($this->value); }
上邊的代碼基本是按照上述的三個步驟來進行的,好好看一下不難理解。
接下來就是模板文件了:
<html> {$data},{$person} <br/>列表一:<br/> <ul> {foreach $arr1} <li>$v</li> {/foreach} </ul> <br/>列表二:<br/> <ul> {loop $arr2} <li>$v</li> {/loop} </ul> {if $a == '1'}a等於1 {elseif $a == '2'}a等於2 {else}a不等於1也不等於2 {/if} </html>
這個模板文件主要測試了之前我們事先的模板標簽。
下面寫一個測試文件:
<?php include_once './Template.php'; $tpl = new Template(); $tpl->assign('data', 'hello'); $tpl->assign('person', 'world!'); $tpl->assign('arr1', array('123','456','789')); $tpl->assign('arr2', array('abc', 'def', 'ghi')); $tpl->assign('a', '2'); $tpl->show('member'); ?>
這就是一個簡單的測試我們的模板引擎能不能用的測試用例。
下面我們看看完整代碼吧:
<?php /** * 模板引擎基礎類 */ class Template { private $config = array( 'suffix' => '.m', // 設置模板文件的后綴 'templateDir' => 'template/', // 設置模板所在的文件夾 'compileDir' => 'cache/', // 設置編譯后存放的目錄 'cache_html' => true, // 是否需要編譯成靜態的HTML文件 'suffix_cache' => '.html', // 設置編譯文件的后綴 'cache_time' => 7200, // 多長時間自動更新,單位秒 'php_turn' => true, // 是否支持原生PHP代碼 'cache_control' => 'control.dat', 'debug' => false, ); private static $instance = null; private $value = array(); // 值棧 private $compileTool; // 編譯器 public $file; // 模板文件名,不帶路徑 public $debug = array(); // 調試信息 private $controlData = array(); public function __construct($config = array()) { $this->debug['begin'] = microtime(true); $this->config = $config + $this->config; if (! is_dir($this->config['templateDir'])) { exit("模板目錄不存在!"); } if (! is_dir($this->config['compileDir'])) { mkdir($this->config['compileDir'], 0770); } $this->getPath(); include './Compile.php'; } /** *獲取絕對路徑 */ public function getPath() { $this->config['templateDir'] = strtr(realpath($this->config['templateDir']), '\\', '/').'/'; $this->config['compileDir'] = strtr(realpath($this->config['compileDir']), '\\', '/').'/'; } /** *取得模板引擎的實例 */ public static function getInstance() { if (is_null(self::$instance)) { self::$instance = new self(); } return self::$instance; } /* 設置模板引擎參數 */ public function setConfig($key, $value = null) { if (is_array($key)) { $this->config = $key + $this->config; }else { $this->config[$key] = $value; } } /* 獲取當前模板引擎配置,僅供調試使用 */ public function getConfig($key = null) { if ($key) { return $this->config[$key]; }else { return $this->config; } } /** *注入單個變量 */ public function assign($key, $value) { $this->value[$key] = $value; } /** *注入數組變量 */ public function assignArray($array) { if (is_array($array)) { foreach($array as $k => $v) { $this->value[$k] = $v; } } } /** * 獲取模板文件完整路徑 */ public function path() { return $this->config['templateDir'].$this->file.$this->config['suffix']; } /** *判斷是否開啟了緩存 */ public function needCache() { return $this->config['cache_html']; } /** *是否需要重新生成靜態文件 */ public function reCache($file) { $flag = true; $cacheFile = $this->config['compileDir'].md5($file).$this->config['suffix_cache']; if ($this->config['cache_html'] === true) { $timeFlag = (time() - @filemtime($cacheFile)) < $this->config['cache_time'] ? true : false; if (is_file($cacheFile) && filesize($cacheFile) > 1 && $timeFlag && filemtime($compileFile) >= filemtime($this->path())) { $flag = false; }else { $flag = true; } } return $flag; } /** *顯示模板 */ public function show($file) { $this->file = $file; if (! is_file($this->path())) { exit('找不到對應的模板!'); } $compileFile = $this->config['compileDir'].md5($file).'.php'; $cacheFile = $this->config['compileDir'].md5($file).$this->config['suffix_cache']; extract($this->value, EXTR_OVERWRITE); if ($this->config['cache_html'] === true) { // 開啟緩存的處理邏輯 if ($this->reCache($file) === true) { // 需要更新緩存的處理邏輯 $this->debug['cached'] = 'false'; $this->compileTool = new Compile($this->path(), $compileFile, $this->config); if ($this->needCache()) {ob_start();} // 打開輸出控制緩沖 if (! is_file($compileFile) || filemtime($compileFile) < filemtime($this->path())) { $this->compileTool->value = $this->value; $this->compileTool->compile(); include $compileFile; }else { include $compileFile; } if ($this->needCache()) { $message = ob_get_contents(); // 獲取輸出緩沖中的內容(在include編譯文件的時候就有輸出了) file_put_contents($cacheFile, $message); } }else { readfile($cacheFile); $this->debug['cached'] = 'true'; } }else { if (! is_file($compileFile) || filemtime($compileFile) < filemtime($this->path())) { $this->compileTool = new Compile($this->path(), $compileFile, $this->config); $this->compileTool->value = $this->value; $this->compileTool->compile(); include $compileFile; }else { include $compileFile; } } $this->debug['spend'] = microtime(true) - $this->debug['begin']; $this->debug['count'] = count($this->value); //$this->debug_info(); } public function debug_info() { if ($this->config['debug'] === true) { echo PHP_EOL, '---------debug info---------', PHP_EOL; echo "程序運行日期:", date("Y-m-d H:i:s"), PHP_EOL; echo "模板解析耗時:", $this->debug['spend'], '秒', PHP_EOL; echo '模板包含標簽數目:', $this->debug['count'], PHP_EOL; echo '是否使用靜態緩存:', $this->debug['cached'], PHP_EOL; echo '模板引擎實例參數:', var_dump($this->getConfig()); } } /** *清理緩存的HTML文件 */ public function clean($path = null) { if ($path === null) { $path = $this->config['compileDir']; $path = glob($path.'* '.$this->config['suffix_cache']); }else { $path = $this->config['compileDir'].md5($path).$this->config['suffix_cache']; } foreach((array)$path as $v) { unlink($v); } } } ?>
<?php class Compile { private $template; // 待編譯的文件 private $content; // 需要替換的文件 private $comfile; // 編譯后的文件 private $left = '{'; // 左定界符 private $right = '}'; // 右定界符 private $value = array(); // 值棧 private $phpTurn; private $T_P = array(); // 匹配正則數組 private $T_R = array(); // 替換數組 public function __construct($template, $compileFile, $config) { $this->template = $template; $this->comfile = $compileFile; $this->content = file_get_contents($template); if ($config['php_turn'] === true) { $this->T_P[] = "#<\?(=|php|)(.+?)\?#is"; $this->T_R[] = "<?\1\2?>"; } // 變量匹配 // \x7f-\xff表示ASCII字符從127到255,其中\為轉義,作用是匹配漢字 $this->T_P[] = "#\{\\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\}#"; // foreach標簽盤匹配 $this->T_P[] = "#\{(loop|foreach)\s+\\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\s*\}#i"; $this->T_P[] = "#\{\/(loop|foreach|if)\}#"; $this->T_P[] = "#\{([k|v])\}#"; // if else標簽匹配 $this->T_P[] = "#\{if (.*?)\}#"; $this->T_P[] = "#\{(else if|elseif) (.*?)\}#"; $this->T_P[] = "#\{else\}#"; $this->T_P[] = "#\{(\#|\*)(.*?)(\#|\*)\}#"; $this->T_R[] = "<?php echo \$this->value['\\1']; ?>"; $this->T_R[] = "<?php foreach ((array)\$this->value['\\2'] as \$k => \$v) { ?>"; $this->T_R[] = "<?php } ?>"; $this->T_R[] = "<?php echo \$\\1?>"; $this->T_R[] = "<?php if(\\1){ ?>"; $this->T_R[] = "<?php }elseif(\\2){ ?>"; $this->T_R[] = "<?php }else{ ?>"; $this->T_R[] = ""; } public function compile() { $this->c_var(); //$this->c_staticFile(); file_put_contents($this->comfile, $this->content); } public function c_var() { $this->content = preg_replace($this->T_P, $this->T_R, $this->content); } /* 對引入的靜態文件進行解析,應對瀏覽器緩存 */ public function c_staticFile() { $this->content = preg_replace('#\{\!(.*?)\!\}#', '<script src=\1'.'?t='.time().'></script>', $this->content); } public function __set($name, $value) { $this->$name = $value; } public function __get($name) { return $this->$name; } } ?>
模板文件member.m代碼:
<html> {$data},{$person} <br/>列表一:<br/> <ul> {foreach $arr1} <li>$v</li> {/foreach} </ul> <br/>列表二:<br/> <ul> {loop $arr2} <li>$v</li> {/loop} </ul> {if $a == '1'}a等於1 {elseif $a == '2'}a等於2 {else}a不等於1也不等於2 {/if} </html>
測試用例:
<?php include_once './Template.php'; $tpl = new Template(); $tpl->assign('data', 'hello'); $tpl->assign('person', 'world!'); $tpl->assign('arr1', array('123','456','789')); $tpl->assign('arr2', array('abc', 'def', 'ghi')); $tpl->assign('a', '2'); $tpl->show('member'); ?>
本文內容大部分來自於《PHP核心技術與最佳實踐》的第六章。