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

PHP 自研模板引擎核心原理與完整實作解析

     編輯:PHP 綜合知識

設計目標

輕量級、編譯型、支援繼承/區塊、語法簡潔(參考 Smarty `<{ }>` 風格)、自動快取編譯結果、支援佈局/區塊/引入/迴圈/條件判斷。

核心類別架構

class Template {
    // 配置與路徑
    private $conf = [];
    private $template_c = '';      // 編譯快取目錄
    private $template_path = '';   // 模板來源目錄
    private $template_name = '';   // 當前模板檔名
    
    // 標籤定義
    private $tag_left = '<{';
    private $tag_right = '}>';
    private $tpl_suffix = '.html';
    private $compile_suffix = '.tpl.php';
    
    // 模板變數儲存
    private $vars = [];
    
    // 標籤屬性定義
    private $tag_foreach = ['from', 'item', 'key'];
    private $tag_include = ['file'];
    
    public function __construct($conf) {
        $this->conf = &$conf;
        $this->template_c = $conf['template_config']['template_c'];
    }
    
    // 變數賦值
    public function assign($key, $value) {
        $this->vars[$key] = $value;
    }
    
    // 主入口:渲染輸出
    public function display($filename = '', $view_path = '') {
        if (!$this->get_tpl($filename, $view_path)) {
            throw new Exception("模板不存在: {$filename}{$this->tpl_suffix}");
        }
        $this->compile();
    }
    // ... 編譯、解析、快取邏輯見下文
}

編譯流程核心

1. 模板檔案定位

private function get_tpl($filename, $view_path) {
    $filename = $filename ?: 'index';
    foreach ($this->conf['view_path'] as $basePath) {
        $fullPath = $view_path 
            ? $basePath . $view_path . $filename . $this->tpl_suffix
            : $basePath . $this->get_tpl_path() . $filename . $this->tpl_suffix;
            
        if (is_file($fullPath)) {
            $this->template_path = dirname($fullPath) . '/';
            $this->template_name = basename($fullPath);
            return true;
        }
    }
    return false;
}

// 根據 目錄/控制器/方法 自動推導路徑
private function get_tpl_path($base = '') {
    $dir = core::get_directory_name();    // 如 admin
    $ctl = core::get_controller_name();   // 如 index
    return $base . "{$dir}/{$ctl}/";
}

2. 編譯快取機制

private function compile() {
    $compileDir = $this->check_temp_compile();
    $compileName = str_replace($this->tpl_suffix, '', $this->template_name);
    $compileFile = $compileDir . $compileName . $this->compile_suffix;
    
    // 快取有效性檢查:來源檔案 mtime <= 編譯檔 mtime
    $recompile = !is_file($compileFile) 
              || filemtime($this->template_path . $this->template_name) > filemtime($compileFile)
              || defined('DEBUG');  // 開發模式強制重編譯
              
    if ($recompile) {
        $source = $this->read_file($this->template_path . $this->template_name);
        $compiled = $this->template_replace($source, $compileDir, $compileName);
        $this->compile_file($compileName, $compiled, $compileDir);
    }
    
    // 執行編譯後的 PHP
    extract($this->vars, EXTR_SKIP);
    include $compileFile;
}

3. 模板語法解析器(核心)

private function template_replace($str, $compileDir, $name) {
    // 遞迴處理 include 標籤(編譯期展開)
    $str = $this->parse_include($str, $compileDir);
    
    // 主體解析:foreach / if / elseif / else / 通用標籤
    $str = $this->parse($str);
    
    // 加入安全標頭與編譯資訊
    return $this->body_content($str);
}

private function parse($content) {
    $content = $this->parse_foreach($content);
    $content = $this->parse_include($content);  // 二次處理巢狀 include
    $content = $this->parse_if($content);
    $content = $this->parse_elseif($content);
    $content = $this->parse_comm($content);     // 結束標籤 /foreach /if /else
    $content = $this->parse_php($content);      // 剩餘標籤視為原生 PHP
    return $content;
}

標籤解析具體實作

foreach 迴圈

// 模板:<{foreach from=$list item=val key=k}>
// 編譯: $val) { ?>
private function parse_foreach($content) {
    $matches = $this->preg_match_all("foreach\s+(.*?)", $content);
    foreach ($matches[1] as $i => $attrStr) {
        $attrs = $this->parse_attrs($attrStr, $this->tag_foreach);
        extract($attrs);  // $from, $item, $key
        
        $keyPart = $key ? '$' . $key . ' => ' : '';
        $php = "<?php foreach(\${$from} as {$keyPart}\${$item}) { ?>";
        $content = str_replace($matches[0][$i], $php, $content);
    }
    return $content;
}

條件判斷

// <{if condition}> → 
private function parse_if($content) {
    $matches = $this->preg_match_all("if\s+(.*?)", $content);
    foreach ($matches[1] as $i => $cond) {
        $php = "<?php if({$cond}) { ?>";
        $content = str_replace($matches[0][$i], $php, $content);
    }
    return $content;
}

// <{elseif condition}> → 
private function parse_elseif($content) {
    $matches = $this->preg_match_all("elseif\s+(.*?)", $content);
    foreach ($matches[1] as $i => $cond) {
        $php = "<?php } elseif ({$cond}) { ?>";
        $content = str_replace($matches[0][$i], $php, $content);
    }
    return $content;
}

// 通用結束標籤
private function parse_comm($content) {
    $search = [
        "/{$this->tag_left}\/foreach{$this->tag_right}/is",
        "/{$this->tag_left}\/if{$this->tag_right}/is",
        "/{$this->tag_left}else{$this->tag_right}/is",
    ];
    $replace = [
        "<?php } ?>",
        "<?php } ?>",
        "<?php } else { ?>",
    ];
    return preg_replace($search, $replace, $content);
}

變數輸出與原生 PHP

// <{$var}> → 
// <{php code}> → 
private function parse_php($content) {
    return preg_replace(
        "/{$this->tag_left}(.+?){$this->tag_right}/is",
        "<?php \$1 ?>",
        $content
    );
}

模板引入(編譯期展開)

// <{include file="public/header.html"}>
private function parse_include($content, $compileDir) {
    $matches = $this->preg_match_all("include\s+(.*?)", $content);
    foreach ($matches[1] as $i => $attrStr) {
        $attrs = $this->parse_attrs($attrStr, $this->tag_include);
        $file = $attrs['file'] ?? '';
        
        // 尋找被引入模板
        foreach ($this->conf['view_path'] as $base) {
            $incPath = $base . $this->view_path_param . $file;
            if (is_file($incPath)) {
                // 遞迴編譯被引入模板
                $incSource = $this->read_file($incPath);
                $incName = basename($file, $this->tpl_suffix);
                $incCompiled = $this->template_replace($incSource, $compileDir, $incName);
                break;
            }
        }
        
        // 替換為 PHP include
        $content = str_replace(
            $matches[0][$i], 
            "<?php include('{$incCompiled}') ?>", 
            $content
        );
    }
    return $content;
}

模板使用範例

佈局模板 layout.html

<!DOCTYPE html>
<html>
<head><title><{$title}></title></head>
<body>
    <{include file="public/header.html"}>
    <main><{$content}></main>
    <{include file="public/footer.html"}>
</body>
</html>

內容模板 index.html

<{foreach from=$articles item=art}>
<article>
    <h2><{$art.title}></h2>
    <time><{$art.created_at|date_format:'%Y-%m-%d'}></time>
    <{if $art.is_top}><span class="badge">置頂</span><{/if}>
    <p><{$art.excerpt}></p>
</article>
<{/foreach}>

控制器呼叫

$tpl = new Template($config);
$tpl->assign('title', '最新文章');
$tpl->assign('articles', $articleList);
$tpl->display('index');  // 自動尋找 tpl/目錄/index.html

效能優化建議

  • OPcache 配合:編譯後的 .tpl.php 會被 OPcache 快取,生產環境效能接近原生 PHP
  • 快取失效策略:生產環境關閉 DEBUG,僅在模板檔案變更時重編譯
  • 靜態化進階:對高頻頁面,可在編譯階段直接生成靜態 HTML

安全考量

  1. 編譯目錄需設定 open_basedir 或防止直接訪問
  2. 模板中禁用危險函式(exec, eval 等),可在 parse_php 階段過濾
  3. 變數輸出預設 HTML 轉義,防 XSS
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved