从零手写PHP模板引擎,顺便理解前端模板语法
开场:模板引擎,被包装得很神秘
在这个前后端分离、框架林立、`v-for` 和 `map()` 信手拈来的年代,很多人已经不再关心“页面上的数据是怎么渲染出来的”了——反正框架帮你搞定了。但如果你曾经被某个魔改的 PHP 项目折磨过,或者面试时被问到“模板引擎的原理是什么”,你就会发现,这东西本质上并没有那么玄乎。
今天我们不打算引入 Composer 包,也不用任何框架的原生模板,纯粹用 PHP 从零开始手写一个“能用但不简陋”的模板引擎。写完之后你会发现:什么 Vue 的插值语法,什么 Django 的 `{{ variable }}`,万变不离其宗,都是同一波操作。
第一步:先搞清楚模板引擎到底在干嘛
模板引擎的使命只有一句话:把模板字符串 + 数据源 = 输出 HTML。
模板本质是一个 带占位符的字符串,数据是一堆变量,引擎负责把占位符替换成数据实际的内容。核心动作是编译和渲染:
- 编译:把模板文字翻译成 PHP 可执行的代码(或者 AST)。
- 渲染:执行这段代码,输入数据,得到最终的 HTML。
我们直接上手写一个最简单的版本——先用字符串替换解决,再增加控制结构支持。
第二步:手写一个超级简单的 PHP 模板引擎
我们先从一个极简的替换类开始:
class SimpleTemplate {
protected $template;
public function __construct($template) {
$this->template = $template;
}
public function render(array $data): string {
$result = $this->template;
foreach ($data as $key => $value) {
$result = str_replace("{{{$key}}}", $value, $result);
}
return $result;
}
}
// 使用示例
$html = '<h1>Hello, {{name}}!</h1><p>你是{{age}}岁。</p>';
$engine = new SimpleTemplate($html);
echo $engine->render(['name' => '小明', 'age' => 18]);
看到没?就是 `str_replace` 的高级马甲。
但这显然不够——遇到 `foreach`、`if` 条件怎么办?如果数据是数组,不允许进行变量点语法?单纯替换解决不了循环结构的问题。所以需要进入第二步:编译成 PHP 代码,然后 `eval` 它。
第三步:进阶版:支持循环和判断
我们先定一下我们要支持的语法风格:
- 变量:`{$var}`(吸 PHP 原生语法)
- 循环:`{foreach $list as $item}` ... `{/foreach}`
- 判断:`{if $ok}` ... `{else}` ... `{/if}`
编译器的工作逻辑是:找到这些自定义标签,替换成 PHP 原生代码,其余部分原样输出。
class TemplateEngine {
public function compile(string $template): string {
// 转义 PHP 标签(防止模板内写原生 PHP,仅为了安全示例)
$pattern = [
'/\{\{\s*\$(\w+)\s*\}\}/' => '<?php echo $1; ?>',
'/\{foreach\s+(\$\w+)\s+as\s+(\$\w+)\}/' => '<?php foreach($1 as $2): ?>',
'/\{\/foreach\}/' => '<?php endforeach; ?>',
'/\{if\s+(.+?)\}/' => '<?php if($1): ?>',
'/\{else\}/' => '<?php else: ?>',
'/\{\/if\}/' => '<?php endif; ?>',
];
$compiled = preg_replace(array_keys($pattern), array_values($pattern), $template);
return '?>' . $compiled . '<?php';
}
public function render(string $template, array $data): string {
$compiledFile = tempnam(sys_get_temp_dir(), 'tpl_');
file_put_contents($compiledFile, $this->compile($template));
extract($data, EXTR_SKIP);
ob_start();
include $compiledFile;
$output = ob_get_clean();
unlink($compiledFile);
return $output;
}
}
// 使用示例:
$tpl = '<ul>{foreach $users as $user}<li>{$user}</li>{/foreach}</ul>
{if $isAdmin}<p>管理员你好</p>{else}<p>普通用户</p>{/if}';
$engine = new TemplateEngine();
echo $engine->render($tpl, ['users' => ['小明', '小红'], 'isAdmin' => true]);
这段核心代码很有意思,它其实做了一件所有现代流行模板引擎的“本质动作”:语法转换 + include 执行。像 Smarty、Blade,本质上都是这个路子,只是它们做得更规范,加入了缓存、函数调用、美化转义。
第四步:一句话讲透前端模板语法
你再看 Vue 的模板:
<li v-for="user in users">{{ user }}</li>
Vue 在编译阶段也是把这段 HTML 模板解析成 AST,然后再生成所谓的“渲染函数”(render function),这个函数内部执行时依赖一个 `_c` 或 `with(this){...}` 的机制来访问数据。本质上就是一个“模板字符串到JS函数的转译”。
React 的 JSX 也大同小异:JSX 语法糖最终被 Babel 转译成 `React.createElement` 嵌套调用,这个和 `{foreach}{/foreach}`被替换为 `<?php foreach ?>` 是同构的。
所以你看,所谓模板引擎,核心就是两个能力:
1. 识别模板中的变量/结构语法。
2. 用目标语言的能力执行生成出的代码(或直接生成一份对应语言的渲染函数)。
前端模板语法里那句“不要写复杂逻辑,只要简单表达式和循环判断”的社区共识,并不是因为模板引擎开发者不想支持,而是因为解析器能力边界和最佳实践的限制。模板就应该是“从数据到展示”的轻量映射层,不承载业务逻辑。
结尾:从手写到理解,再去拥抱框架
自己写完一个几十行的 PHP 模板引擎,你会有一种“原来框架里那段‘自研模板’也没多复杂”的释然感。当然,生产环境中你不会去手写它——你已经有了 Blade、Twig、Vue、Svelte 这些成熟选择。
但理解这块拼图之后,你会发现很多东西底层原先是相通的:**从前,我们都是模板字符串 / 模板语法;现在,无非是在编译的前后步骤里多了 AST 和 DOM diff 而已。**
手里的轮子自己造一遍,只是为了一眼看穿它背后那套简洁逻辑。
年卡会员