写PHP的人几乎都经历过这样一个阶段:一个index.php文件从数据库连接、业务判断一直写到最后的HTML标签,几千行代码里PHP和HTML互相穿插,改一个页面样式要在一堆<?php ?>之间来回跳。这种写法到底是不是错的?如果改用模板引擎做分离,又该怎么做才不算过度设计?这篇文章就把两种方式摊开来讲清楚。

混合式写法:快在哪里,乱在哪里
混合式写法指的是在同一个文件里同时存在PHP逻辑和HTML结构,PHP负责取数据、做判断,HTML负责呈现。这种写法最大的优势是直接,不需要额外的抽象层,浏览器请求一个文件,服务器处理完就直接输出,没有模板编译、没有变量传递的开销。对于一次性脚本、内部小工具、或者只有几个页面的微型项目来说,混合式完全够用。
来看一个典型的混合式文件:
<?php
// logic.php 数据查询
$pdo = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8mb4', 'root', '');
$stmt = $pdo->query('SELECT id, title FROM articles ORDER BY id DESC LIMIT 10');
$articles = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>文章列表</title></head>
<body>
<h1>文章列表</h1>
<ul>
<?php foreach ($articles as $a): ?>
<li><?php echo htmlspecialchars($a['title'], ENT_QUOTES, 'UTF-8'); ?></li>
<?php endforeach; ?>
</ul>
</body>
</html>注意上面用到的foreach (): ... endforeach;这种替代语法,它比花括号更适合在HTML中穿插,可读性明显更好。同样适用的还有if: ... endif;和while: ... endwhile;。写混合式代码时务必用这种语法,否则嵌套一深就完全看不清结构了。
混合式的隐患主要有三个。第一,业务逻辑和展示逻辑容易越搅越浑,一开始只是在模板里做个if判断,慢慢就会有人把SQL查询、权限校验直接写进HTML中间。第二,无法复用,同一段页面头部在十个文件里复制十份,改一次导航栏要动十个文件。第三,前端和后端没法并行工作,改页面的人必须懂PHP,改PHP的人天天被HTML细节打扰。当项目超过三五个页面、或者有多人协作时,这些问题会集中爆发。
分离式开发:从最朴素的include开始
分离式并不是必须上模板引擎,PHP自带的include和require就能完成最基本的分离。核心思路是:逻辑文件只负责准备数据,模板文件只负责接收数据并渲染,中间通过变量传递。下面把上面的例子拆成两个文件。
逻辑文件list.php:
<?php
// list.php 只做数据处理
$pdo = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8mb4', 'root', '');
$stmt = $pdo->query('SELECT id, title FROM articles ORDER BY id DESC LIMIT 10');
$articles = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 渲染交给模板
require __DIR__ . '/templates/list.tpl.php';模板文件templates/list.tpl.php:
<?php /* list.tpl.php 纯展示,不做数据库操作 */ ?> <!DOCTYPE html> <html> <head><meta charset="utf-8"><title>文章列表</title></head> <body> <?php include __DIR__ . '/partials/header.tpl.php'; ?> <ul> <?php foreach ($articles as $a): ?> <li><?php echo htmlspecialchars($a['title'], ENT_QUOTES, 'UTF-8'); ?></li> <?php endforeach; ?> </ul> <?php include __DIR__ . '/partials/footer.tpl.php'; ?> </body> </html>
这种原生模板方式有几个值得坚持的规矩:模板文件第一行写注释声明它只依赖传入的变量;模板里出现的数据一律用htmlspecialchars转义,防止XSS;公共的头部、尾部抽成partial文件,通过include复用。做到这三点,即使不用任何第三方库,代码结构也能维持基本的整洁。
原生模板的缺点也很明显:没有自动转义,忘写一次htmlspecialchars就是一个安全漏洞;没有布局继承机制,公共结构还是靠手动include拼装;模板里照样可以写任意PHP代码,约束全靠团队自觉。当项目规模继续增长,就需要真正的模板引擎来补上这些短板。
模板引擎方案:Twig与Blade的实际用法
主流的PHP模板引擎里,Twig和Blade最有代表性。Twig是独立库,可以配合任何框架使用;Blade则是Laravel框架内置的。它们的共同点是:模板语法受限,只能做展示层的判断和循环,写不了数据库查询;输出默认自动转义;支持布局继承和区块覆盖。
用Twig改写上面的列表页,模板大概长这样:
{# templates/list.twig #}
{% extends 'base.twig' %}
{% block title %}文章列表{% endblock %}
{% block content %}
<ul>
{% for article in articles %}
<li>{{ article.title }}</li>
{% endfor %}
</ul>
{% endblock %}{{ }}输出变量时会自动做HTML转义,想输出原始HTML得显式写成{{ article.content|raw }},这等于把安全决策从默认放行改成了默认拦截,大幅降低了XSS风险。{% extends %}配合{% block %}实现了布局继承,公共的头部导航写在base.twig里一次即可,子模板只覆盖需要变化的部分,彻底告别手动拼装partial的日子。
Blade的思路类似,语法更轻量一些:
@extends('layouts.base')
@section('title', '文章列表')
@section('content')
<ul>
@foreach ($articles as $article)
<li>{{ $article->title }}</li>
@endforeach
</ul>
@endsectionBlade的{{ }}同样默认调用e()函数转义,@foreach本质上是PHP语法的语法糖,最终会被编译成原生PHP文件缓存执行,性能几乎无损。需要警惕的一点是,Blade允许@php ... @endphp嵌入任意PHP代码,这个口子一旦被滥用,模板引擎的约束就形同虚设,团队规范里应该明确限制它的使用范围。
怎么选:按项目规模和维护周期决策
选型没有绝对答案,但可以给出几条比较清晰的判断线。第一,页面数量在三五个以内、生命周期短、只有一个人维护的内部工具,混合式完全可以接受,强行引入模板引擎反而增加学习成本和目录复杂度。第二,页面超过十个、或预期要长期维护的项目,至少要做到原生模板分离,把数据准备和渲染拆到不同文件。第三,多人协作尤其是前后端分工的项目,直接上Twig或Blade,受限的模板语法能从机制上防止业务逻辑渗透进视图层,自动转义也省去了逐个检查的精力。
还有一点常被忽略:分离式开发的价值不只是代码好看。当逻辑和视图拆开后,同一段逻辑可以输出网页、JSON接口甚至邮件模板,只需要换一个渲染入口;单元测试也可以只针对逻辑文件写,不必启动整个HTTP环境。这些收益在项目初期不明显,但在第二期、第三期需求进来之后会越来越重要。所以如果你还在犹豫,宁可一开始就多做一步分离,也不要等文件膨胀到几千行再回头重构,那时代价会大得多。