首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP MVC 模板引擎核心實現:正則解析、標簽體系、緩存機制與遞歸嵌套處理

     編輯:PHP 綜合知識

模板引擎是 MVC 架構中「視圖層」的核心,將業務邏輯與展示邏輯分離。本文深度解析一個基於正則表達式、支援 {data}/{list} 標簽、遞歸嵌套、條件判斷、循環、PHP 原生代碼嵌入、文件緩存的完整模板引擎實現。

架構設計:雙類協作

  • Template 類:模板文件加載、標簽識別、分發調度、輸出緩存
  • Tags 類:具體標簽語義解析、數據查詢、遞歸處理、PHP 代碼編譯執行

核心類實現解析

Template 類:調度中樞

<?php
class Template {
    public string $html = '';
    public array $vars = [];
    public string $bFlag = '{';
    public string $eFlag = '}';
    public string $pfix = 'zmm:';
    private string $folder = '';
    private string $file = '';
    
    public function __construct(array $vars = []) {
        $this->vars = $vars;
        $this->bTag = $this->bFlag . $this->pfix;
        $this->eTag = $this->bFlag . '/' . $this->pfix;
        Tags::$vars = &$this->vars; // 靜態引用共享變量
    }
    
    // 加載模板文件
    public function LoadTpl(string $tpl): void {
        $this->file = $this->GetTplPath($tpl);
        Tags::$file = &$this->file;
        
        if (!is_file($this->file)) exit("模板文件 [{$this->file}] 不存在!");
        
        $html = file_get_contents($this->file);
        $html = self::FmtTplHtml($html); // 壓縮:去除換行、制表、多餘空格
        
        // 語法預編譯:{if} {loop} {php} 等轉為 PHP 代碼
        $callFunc = Tags::$prefix . 'Syntax';
        $this->html = Tags::$callFunc($html, $this);
    }
    
    // 壓縮模板 HTML
    static public function FmtTplHtml(string $html): string {
        return preg_replace('/(\r)|(\n)|(\t)|(\s{2,})/is', '', $html);
    }
    
    // 標簽解析入口
    private function SetTplTags(): void {
        $this->SetPanelTags();   // 單標簽 {zmm:tag attr/}
        $this->SetTrunkTags();   // 雙標簽 {zmm:tag attr}content{/zmm:tag}
        $this->RegHatchVars();   // 遞歸處理嵌套
    }
    
    // 單標簽匹配:{zmm:tag.attr/} 或 {zmm:tag attr/}
    private function SetPanelTags(): void {
        $rule = $this->bTag . '([^' . $this->eFlag . ']+)\\/' . $this->eFlag;
        preg_match_all('/' . $rule . '/ism', $this->html, $matches);
        $this->TransTag($matches, 'panel');
    }
    
    // 雙標簽匹配:{zmm:tag attr} ... {/zmm:tag}(支援遞歸嵌套)
    private function SetTrunkTags(): void {
        $rule = $this->bTag . '(\\w+)\\s*([^' . $this->eFlag . ']*?)' . $this->eFlag .
                '((?:(?!' . $this->bTag . ')[\\S\\s]*?|(?R))*)' .
                $this->eTag . '\\1\\s*' . $this->eFlag;
        preg_match_all('/' . $rule . '/ism', $this->html, $matches);
        $this->TransTag($matches, 'trunk');
    }
    
    // 分發給 Tags 類處理
    private function TransTag(array $matches, string $type): void {
        if (empty($matches[0])) return;
        
        foreach ($matches[0] as $i => $fullTag) {
            $callFunc = Tags::$prefix . ucfirst($type === 'panel' ? 
                (strpos($matches[1][$i], '.') ? explode('.', $matches[1][$i])[0] : 
                 explode(' ', trim($matches[1][$i]))[0]) : 
                $matches[1][$i]);
            
            if (method_exists('Tags', $callFunc)) {
                $html = $type === 'panel' 
                    ? Tags::$callFunc($matches[1][$i]) 
                    : Tags::$callFunc($matches[2][$i], $matches[3][$i]);
                if ($html !== false) {
                    $this->html = str_ireplace($fullTag, $html, $this->html);
                }
            }
        }
    }
    
    // 遞歸處理嵌套標簽
    private function RegHatchVars(): void {
        if (preg_match('/' . $this->bTag . '/ism', $this->html)) {
            $this->SetTplTags(); // 還有標簽則繼續解析
        }
    }
    
    // 輸出
    public function Display(bool $cache = false, string $name = '', int $time = 0): void {
        if ($cache && $name) {
            $cacheObj = new Cache($time ?: 600);
            $cacheObj->Set($name, $this->html);
        }
        echo $this->html;
        flush();
    }
}
?>

Tags 類:語義處理核心

<?php
class Tags {
    static public string $prefix = 'TAG_';
    static public array $vars = [];
    static public string $file = '';
    static private string $rule = '';
    
    // 語法預編譯:模板控制結構 → PHP 代碼
    static public function TAG_Syntax(string $html, Template $tpl): string {
        $bTag = $tpl->bTag; $eTag = $tpl->eTag; $eFlag = $tpl->eFlag;
        
        $html = preg_replace('/' . $bTag . 'if\\s+([^' . $eFlag . ']+)\\s*' . $eTag . '/ism', '', $html);
        $html = preg_replace('/' . $bTag . 'elseif\\s+([^' . $eFlag . ']+)\\s*' . $eTag . '/ism', '', $html);
        $html = preg_replace('/' . $bTag . 'else\\s*' . $eTag . '/ism', '', $html);
        $html = preg_replace('/' . $bTag . 'loop\\s+(\\S+)\\s+(\\S+)\\s*' . $eTag . '/ism', '', $html);
        $html = preg_replace('/' . $bTag . 'loop\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s*' . $eTag . '/ism', ' \\3) { ?>', $html);
        $html = preg_replace('/' . $eTag . '(if|loop)\\s*' . $eTag . '/ism', '', $html);
        $html = preg_replace('/' . $bTag . 'php\\s*' . $eTag . '((?:(?!' . $bTag . ')[\\S\\s]*?|(?R))*)' . $eTag . 'php\\s*' . $eTag . '/ism', '', $html);
        
        return self::TAG_Execute($html); // 執行編譯後的 PHP
    }
    
    // 執行 PHP 代碼並捕獲輸出
    static public function TAG_Execute(string $html): string {
        ob_clean(); ob_start();
        if (!empty(self::$vars)) extract(self::$vars, EXTR_OVERWRITE);
        
        $file = sys_get_temp_dir() . '/tpl_' . md5(uniqid(rand(), true)) . '.php';
        file_put_contents($file, $html);
        include $file;
        $output = ob_get_contents();
        ob_end_clean();
        unlink($file);
        return $output;
    }
    
    // {list} 標簽:列表數據、支援分頁、嵌套
    static public function TAG_List(string $attr, string $html): string {
        return self::processDataTag($attr, $html, true);
    }
    
    // {data} 標簽:單條數據
    static public function TAG_Data(string $attr, string $html): string {
        return self::processDataTag($attr, $html, false);
    }
    
    // 統一數據處理邏輯
    static private function processDataTag(string $attr, string $html, bool $isList): string {
        if (empty($html)) exit($isList ? '標簽{list}內容為空!' : '標簽{data}內容為空!');
        
        // 若包含子標簽,先佔位遞歸處理
        if (self::TAG_HaveTag($html)) {
            return self::TAG_DealTag($attr, $html, $isList);
        }
        return self::TAG_GetData($attr, $html, $isList);
    }
    
    // 數據查詢與渲染核心
    static private function TAG_GetData(string $attr, string $html, bool $isList): string {
        $attrObj = new Attbt($attr); // 屬性解析類
        $attrs = $attrObj->attrs;
        extract($attrs, EXTR_OVERWRITE); // source, where, order, rows, page, size...
        
        $source = table_name($source, $column); // 表名處理
        $dataSql = new DataSql();
        
        // 構建 SQL
        $whereSql = $where ? ' WHERE ' . str_replace(',', ' AND ', $where) : '';
        $orderSql = $order ? " ORDER BY $order" : self::getDefaultOrder($dataSql, $source);
        
        if ($isList) {
            // 列表查詢 + 分頁
            $limitSql = $rows > 0 ? " LIMIT 0, $rows" : '';
            $sql = $sql ?? "SELECT * FROM `$source`$whereSql$orderSql$limitSql";
            
            if ($page === 'true' && $size > 0) {
                $total = $dataSql->GetRecNum($sql);
                $pageCount = ceil($total / $size);
                $pageNum = max(1, min($GLOBALS['page'] ?? 1, $pageCount));
                $sql = "SELECT * FROM `$source`$whereSql$orderSql LIMIT " . ($pageNum-1)*$size . ", $size";
                // 設置分頁全局變量供模板使用
                $GLOBALS['cfg_page_curr'] = $pageNum;
                $GLOBALS['cfg_page_list'] = function_exists('list_pagelink') ? list_pagelink($pageNum, $pageCount, 2) : '';
            }
            
            // 逐行渲染
            $result = $dataSql->SqlCmdExec($sql);
            $output = '';
            $idx = 0;
            while ($row = $dataSql->GetRecArr($result)) {
                $tpl = $html;
                $tpl = str_ireplace('@idx', $idx++, $tpl);
                // 字段替換 [field:name/]
                preg_match_all('/\[field:\\s*(\\w+)\\s*([^\\]]*?)\\s*\\/?\]/is', $tpl, $fields);
                foreach ($fields[1] as $i => $field) {
                    $val = $row[$field] ?? '';
                    $tpl = str_ireplace($fields[0][$i], $val, $tpl);
                }
                $output .= $tpl;
            }
            return $output;
        } else {
            // 單條查詢
            $sql = "SELECT * FROM `$source`$whereSql$orderSql LIMIT 1";
            $row = $dataSql->GetOneRec($sql);
            if (!$row) return '';
            
            $tpl = $html;
            preg_match_all('/\[field:\\s*(\\w+)\\s*([^\\]]*?)\\s*\\/?\]/is', $tpl, $fields);
            foreach ($fields[1] as $i => $field) {
                $val = $row[$field] ?? '';
                // 支援函數處理 [field:name function="htmlspecialchars(@me)"/]
                if ($fields[2][$i]) {
                    $funcAttr = new Attbt($fields[2][$i]);
                    if ($func = $funcAttr->attrs['function'] ?? '') {
                        eval('$val = ' . str_ireplace('@me', "'$val'", $func) . ';');
                    }
                }
                $tpl = str_ireplace($fields[0][$i], $val, $tpl);
            }
            return $tpl;
        }
    }
    
    // 處理嵌套標簽:佔位 → 遞歸 → 還原
    static private function TAG_DealTag(string $attr, string $html, bool $isList): string {
        preg_match_all('/' . self::$rule . '/ism', $html, $matches);
        $placeholders = [];
        foreach ($matches[3] as $i => $content) {
            $ph = "{-->>child_node_$i<<--}";
            $placeholders[$ph] = $content;
            $html = str_ireplace($content, $ph, $html);
        }
        $html = self::TAG_GetData($attr, $html, $isList);
        $html = str_ireplace(array_keys($placeholders), array_values($placeholders), $html);
        return $html;
    }
    
    // 檢測是否包含子標簽
    static private function TAG_HaveTag(string $html): bool {
        $tpl = new Template();
        self::$rule = $tpl->bTag . '([^' . $tpl->eFlag . ']+)\\/' . $tpl->eTag;
        if (preg_match('/' . self::$rule . '/ism', $html)) return true;
        self::$rule = $tpl->bTag . '(\\w+)\\s*([^' . $tpl->eFlag . ']*?)' . $tpl->eTag .
                      '((?:(?!' . $tpl->bTag . ')[\\S\\s]*?|(?R))*)' .
                      $tpl->eTag . '\\1\\s*' . $tpl->eFlag;
        return (bool)preg_match('/' . self::$rule . '/ism', $html);
    }
    
    // 靜態方法調用兜底
    static public function __callStatic(string $name, array $args): void {
        exit("標簽 {$name} 不存在!");
    }
}
?>

模板語法速查

標簽類型語法說明
變量輸出[field:name/]輸出字段值
函數處理[field:name function="htmlspecialchars(@me)"/]@me 代表字段值
條件判斷{zmm:if $user.vip}...{zmm:elseif $user.level>5}...{zmm:else}...{/zmm:if}支援 PHP 表達式
循環遍歷{zmm:loop $list item}...{/zmm:loop}或 {zmm:loop $list key item}
原生 PHP{zmm:php} echo $var; {/zmm:php}執行任意 PHP 代碼
數據塊{zmm:list source="article" where="cat=1" order="id desc" rows="10"}...{/zmm:list}列表查詢、分頁
單條數據{zmm:data source="user" where="id=1"}...{/zmm:data}單行查詢

關鍵技術點解析

1. 遞歸正則處理嵌套標簽

// 雙標簽正則核心(支援遞歸)
$rule = $bTag . '(\\w+)\\s*([^' . $eFlag . ']*?)' . $eFlag .
        '((?:(?!' . $bTag . ')[\\S\\s]*?|(?R))*)' .  // 關鍵:(?R) 遞歸匹配整個模式
        $eTag . '\\1\\s*' . $eFlag;

(?R) 代表遞歸引用整個正則模式,實現「任意層級嵌套」匹配。

2. 佔位符法處理嵌套渲染

  1. 檢測內容中是否有子標簽
  2. 將子標簽內容替換為唯一佔位符 {-->>child_node_N<<--}
  3. 先渲染外層標簽(獲取數據、替換字段)
  4. 將佔位符還原為子標簽內容
  5. 遞歸處理子標簽(RegHatchVars 循環調用)

3. PHP 代碼編譯執行機制

  • 模板控制結構(if/loop/php)預編譯為原生 PHP
  • 生成臨時 .php 文件 → include 執行 → ob_get_contents 捕獲輸出
  • 利用 extract($vars) 將變量注入符號表
  • 臨時文件執行後立即刪除

4. 文件緩存策略

public function Display(bool $cache = false, string $name = '', int $time = 0): void {
    if ($cache && $name) {
        $cacheObj = new Cache($time ?: 600);
        $cacheObj->Set($name, $this->html); // 緩存最終渲染後的 HTML
    }
    echo $this->html;
}

現代化改造建議

痛點改造方向推薦方案
正則解析脆弱、難維護編譯器模式Lexer + Parser 生成 AST,再編譯為 PHP(如 Twig、Blade)
eval/include 執行不安全沙箱/預編譯預編譯為 PHP 類文件,OpCache 加速,禁用危險函數
全局變量 $GLOBALS 污染依賴注入模板類接收 Context 對象,而非靜態變量
SQL 硬編碼在模板屬性中數據層分離控制器準備數據,模板僅負責渲染(MVVM/ViewModel)
無繼承/佈局機制佈局塊實現 {extend}/{block},支援佈局繼承

總結

這個模板引擎展示了「正則解析 + 遞歸替換 + PHP 編譯執行」的經典實現路徑。其核心亮點在於:

  1. 遞歸正則 + 佔位符 完美解決任意層級標簽嵌套
  2. 語法預編譯 將模板控制結構轉為原生 PHP,執行效率高
  3. 標簽語義分離 Tags 類職責單一,易於擴展新標簽
  4. 緩存機制 支援渲染結果文件緩存

現代專案建議直接使用 Twig、Blade(Laravel)、Lathe 等成熟引擎,它們提供沙箱安全、繼承佈局、異步渲染、IDE 提示等企業級特性。但理解這類「手寫引擎」的核心機制,對於維護舊系統、設計 DSL、或構建輕量級框架極有價值。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved