輕量級、編譯型、支援繼承/區塊、語法簡潔(參考 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();
}
// ... 編譯、解析、快取邏輯見下文
}
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}/";
}
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;
}
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 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);
}
// <{$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;
}
<!DOCTYPE html>
<html>
<head><title><{$title}></title></head>
<body>
<{include file="public/header.html"}>
<main><{$content}></main>
<{include file="public/footer.html"}>
</body>
</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
open_basedir 或防止直接訪問exec, eval 等),可在 parse_php 階段過濾