将数据库中的订单、报表或合同数据导出为PDF,常见的做法是先由模板引擎生成结构完整的HTML,再交给渲染库转换。TinyButStrong(简称TBS)是一款轻量级PHP模板引擎,可以用简单的标签把PHP变量、循环和条件注入HTML模板;Dompdf则是一个PHP库,不依赖浏览器就能把HTML和CSS渲染成PDF文件。两者配合后,服务端可以批量生成排版统一的报价单、对账单等文档。本文以一个产品报价单为例,展示从安装、模板编写到PDF输出的完整过程。

一、TinyButStrong模板引擎基础与安装
TinyButStrong的核心思路是模板与数据分离。模板文件仍然是一个普通的HTML文件,只是内部可以使用TBS定义的变量标签和区块标记。PHP代码负责加载模板、准备数据并执行合并,最终得到完整的HTML字符串。这种设计让前端开发人员可以专注页面结构,后端开发人员只处理数据填充,维护成本比较低。
安装TBS可以通过Composer执行 composer require tinybutstrong/tinybutstrong,也可以从官网下载 tbs_class.php 后手动引入。对于使用Composer的项目,加载自动加载文件后即可实例化。下面是一个最简单的示例:
<?php
require_once 'vendor/autoload.php';
$TBS = new clsTinyButStrong;
$TBS->LoadTemplate('template.html');
$name = '张三';
$TBS->MergeField('name', $name);
$TBS->Show();
?>
对应的模板文件 template.html 可以写成这样:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>报价单</title>
</head>
<body>
<p>客户姓名:[var.name]</p>
</body>
</html>
在上面的模板中,[var.name] 是TBS默认的变量占位符。当调用 MergeField('name', $name) 时,TBS会把模板中所有 [var.name] 替换为对应的值。TBS还支持数组和对象属性的访问,例如 [var.user.name] 可以直接输出嵌套数据。如果希望修改占位符的定界符,可以在初始化后调整配置,但在大多数项目中默认语法已经够用。
二、使用区块合并生成动态HTML
报价单通常包含一个产品列表,行数会随着订单内容变化。如果只用变量替换,很难处理重复的表格行。TinyButStrong提供了区块合并机制,通过在模板中标记区块起止位置,后端传入二维数组,TBS就会自动循环输出每一行。这个机制非常适合表格、列表、发票明细等结构。
先看模板部分。下面是一个产品表格,注意 <!-- Block products --> 和 <!-- End products --> 之间的 <tr> 会被循环输出:
<table border="1" cellpadding="6">
<thead>
<tr>
<th>产品名称</th>
<th>单价</th>
<th>数量</th>
<th>小计</th>
</tr>
</thead>
<tbody>
<!-- Block products -->
<tr>
<td>[products.name]</td>
<td>[products.price]</td>
<td>[products.qty]</td>
<td>[products.subtotal]</td>
</tr>
<!-- End products -->
</tbody>
</table>
后端PHP代码需要准备一个二维数组,并调用 MergeBlock 方法。数组的键名要与模板中的字段名对应:
<?php
$products = array(
array('name' => '机械键盘', 'price' => 299, 'qty' => 2, 'subtotal' => 598),
array('name' => '无线鼠标', 'price' => 89, 'qty' => 3, 'subtotal' => 267),
array('name' => '显示器支架', 'price' => 159, 'qty' => 1, 'subtotal' => 159),
);
$TBS->MergeBlock('products', $products);
$TBS->Show();
?>
执行后,<tbody> 中会出现三行数据。TBS区块合并不仅支持简单的二维数组,还可以处理对象数组和多级嵌套区块。例如在主产品区块内部再嵌套一个规格区块,只需在模板中再写一层 Block specs 和 End specs,后端传入对应字段的数组即可。条件显示也常用:TBS允许在变量标签上附加 onshow 参数,例如 [onshow;if [products.price] > 100;then='高价';else='平价'],这样模板自身就能表达简单逻辑,不需要在PHP里拼装太多HTML片段。
三、Dompdf渲染PDF的完整流程
Dompdf是一个纯PHP的HTML到PDF转换库,安装命令为 composer require dompdf/dompdf。它的工作流程是:加载HTML字符串,解析DOM结构和CSS样式,计算元素位置,最后通过字体和绘图引擎输出PDF。Dompdf支持CSS 2.1的大部分属性以及一些CSS 3特性,但不支持JavaScript,也不适合渲染特别复杂的网页布局。对于常规的报价单、合同、报表,它的表现足够稳定。
把TBS生成的HTML交给Dompdf时,推荐先获取HTML字符串而不是直接输出。可以使用输出缓冲来捕获 Show() 的结果,这样能避免向浏览器发送多余内容。下面是完整示例:
<?php
require_once 'vendor/autoload.php';
use Dompdf\Dompdf;
use Dompdf\Options;
// 准备数据
$products = array(
array('name' => '机械键盘', 'price' => 299, 'qty' => 2, 'subtotal' => 598),
array('name' => '无线鼠标', 'price' => 89, 'qty' => 3, 'subtotal' => 267),
);
// 使用TinyButStrong生成HTML
$TBS = new clsTinyButStrong;
$TBS->LoadTemplate('template.html');
$TBS->MergeBlock('products', $products);
ob_start();
$TBS->Show();
$html = ob_get_clean();
// 配置Dompdf
$options = new Options();
$options->set('isRemoteEnabled', true);
$options->set('isHtml5ParserEnabled', true);
$dompdf = new Dompdf($options);
$dompdf->loadHtml($html);
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();
// 在浏览器中预览,若需要下载则把Attachment改为1
$dompdf->stream('quotation.pdf', array('Attachment' => 0));
?>
代码中的 ob_start() 和 ob_get_clean() 用于捕获TBS输出的HTML,这是比较稳妥的做法。如果不想用输出缓冲,也可以直接访问TBS的源码属性,但不同版本可能存在差异,因此缓冲方式兼容性更好。setPaper 方法可以指定纸张大小和方向,常用参数包括 A4、letter 以及 portrait、landscape。stream 方法会把PDF发送到浏览器,第一个参数是文件名,第二个参数中 Attachment 设为 0 表示在浏览器内预览,设为 1 则强制下载。如果需要把PDF保存到服务器,可以改用 $dompdf->output() 获取二进制内容,再用 file_put_contents 写入磁盘。
四、中文字体与分页控制
中文乱码是Dompdf使用中最常见的问题之一。默认情况下,Dompdf带有的是DejaVu Sans等西文字体,无法正确显示中文。解决方法是在CSS中指定一个可用的中文字体,并确保Dompdf能够找到字体文件。可以通过 @font-face 引入自定义字体,也可以使用Dompdf自带的 load_font.php 脚本注册系统字体。对于线上环境,建议将字体文件放在项目目录中,然后通过相对路径或绝对路径引用。
下面是一个简单的字体声明示例,假设字体文件放在项目的 fonts 目录下:
@font-face {
font-family: 'NotoSansCJKsc';
src: url('/fonts/NotoSansCJKsc-Regular.otf');
font-weight: normal;
font-style: normal;
}
body {
font-family: 'NotoSansCJKsc', sans-serif;
}
如果字体文件位于本地磁盘,还可以在Dompdf的选项中设置 chroot 或开启 isRemoteEnabled 来允许加载。但要注意,开启远程资源加载可能存在安全风险,只应在可信环境下使用。另外,CSS中的分页控制也有不少细节。Dompdf支持 page-break-before、page-break-after 和 page-break-inside 属性。对于表格,建议加上 tr { page-break-inside: avoid; },这样可以防止一行数据被分页切断。如果某个章节需要另起一页,可以给标题元素设置 page-break-before: always;。合理使用这些规则,能够显著提升生成PDF的排版质量。
除了字体和分页,图片路径也需要留意。当HTML中包含本地图片时,Dompdf默认会限制文件访问范围,需要在选项中配置 chroot 或使用完整路径。对于网络图片,可以开启 isRemoteEnabled,但响应时间会变长。实际项目中更推荐先把图片转为绝对路径或使用数据URI,减少渲染阶段的网络依赖。总之,TinyButStrong负责把动态数据准确填进HTML骨架,Dompdf负责把这个骨架变成排版稳定的PDF,两者之间的接口就是HTML字符串。理解了这条链路,后续无论是增加水印、页码还是签名图片,都可以在模板和CSS层面逐步扩展。
TinyButStrongDompdfHTML转PDF修改时间:2026-09-29 01:10:36