各位好,各位后端界的扫地僧、全栈界的浪人、以及那些白天写 PHP,晚上还要偷偷研究排版艺术的极客们。
今天我们不讲“为什么 CRUD 是 CRUD 的宿命”,也不讲“Laravel 11 为什么要抛弃中间件”。今天,我们要聊一个听起来有点“强迫症晚期”的味道,但一旦掌握,就能让你的产品从“电子表格”变成“金融时报”的高大上话题:
如何用 PHP 的野性逻辑,驯服 LaTeX 的严谨美学,最终打印出一张完美的 PDF。
听着,我知道你的反应。“PHP?那不是用来写报错的吗?”“LaTeX?那不是数学系学生写论文用的吗?”
没错。这正是我们要玩的东西。在这个世界上,有两种编程语言:一种是妥协的,一种是完美的。PHP 代表妥协,它随叫随到,脏活累活不挑,排版乱七八糟;而 LaTeX 代表完美,它固执己见,如果你给它的参数不对,它甚至不报错,直接给你一个 ! Missing $ inserted,然后让你在那行代码里坐牢。
我们的任务,就是用 PHP 的“廉价劳动力”去处理数据,然后把数据塞给 LaTeX 这个“高冷甲方”,让它帮我们干脏活。我们要实现从业务逻辑到高精度物理排版的自动转换。
准备好了吗?让我们开始这场“代码与墨水”的博弈。
第一回:为什么你要在 PHP 里玩 LaTeX?
先问一个问题:如果你的业务里出现了这些需求,你头大吗?
- 财务报表: 金额需要千分位,百分比保留四位小数,还要加粗,还要居中,甚至还要用特殊字体区分“实收”和“应收”。
- 技术文档: 包含复杂的公式。比如流体力学方程、矩阵运算、概率分布图。
- 证书/发票: 每一行数据都不同,但页眉页脚、公章位置、字体大小必须分毫不差。
如果你用 PHP 直接生成 HTML 然后转 PDF(比如 dompdf 或 wkhtmltopdf),恭喜你,你得到的是一堆“看起来像那么回事”的垃圾。文字间距不对,数学公式变成乱码,表格对不齐。
而 LaTeX 是怎么做的?它是基于“断字”和“数学建模”的。它计算的是“如果这个字在这个位置,下一行会不会太挤”。它渲染出来的 PDF,是矢量级的,放大一万倍依然清晰。
所以,我们要做的不是“用 PHP 画 PDF”,而是“用 PHP 生成 LaTeX 源码”。
第二回:搭建“桥梁”——PHP 如何指挥 LaTeX
在开始之前,我们需要搭建一个“指挥系统”。LaTeX 本身不能自己生成内容,它需要源文件。PHP 的角色就是那个坐在指挥台上的将军,拿着地图(数据),对 LaTeX 说:“把 A 处的数字改成 3.14,把 B 处的矩阵摆成 3×3。”
在 Linux 服务器(通常是 Docker 容器)上,我们需要安装 LaTeX 环境。如果你在本地 Windows,建议直接装个 TeX Live 或者 MiKTeX。如果你在服务器上,记得安装 texlive-latex-extra 和 texlive-fonts-recommended。
最简单的交互方式,就是通过命令行调用。
代码示例:最原始的“管道”
<?php
class LatexEngine {
private $binaryPath = '/usr/local/texlive/bin/x86_64-linux/pdflatex'; // 你的 pdflatex 路径
private $templatePath = '/var/www/html/templates/latex_template.tex';
public function compile($data) {
// 1. 准备数据,就像填空题一样
$content = file_get_contents($this->templatePath);
$content = str_replace('{TITLE}', $data['title'], $content);
$content = str_replace('{DATE}', date('Y-m-d'), $content);
$content = str_replace('{CONTENT}', $data['body'], $content);
// 2. 写入临时文件
$tempFile = '/tmp/' . uniqid() . '.tex';
file_put_contents($tempFile, $content);
// 3. 调用 LaTeX 引擎(这是最关键的一步,容易报错)
$command = sprintf(
'%s -interaction=nonstopmode %s 2>&1',
escapeshellcmd($this->binaryPath),
escapeshellarg($tempFile)
);
// 4. 执行命令并捕获输出
$output = shell_exec($command);
// 5. 检查是否生成了 PDF
$pdfFile = preg_replace('/.tex$/', '.pdf', $tempFile);
if (file_exists($pdfFile)) {
return file_get_contents($pdfFile);
}
// 如果没生成,说明编译报错了,把错误吐出来
throw new Exception("LaTeX 编译失败:" . $output);
}
}
// 使用
$engine = new LatexEngine();
$pdfData = $engine->compile([
'title' => 'PHP 与 LaTeX 的联姻',
'body' => 'Hello World'
]);
警告: 这段代码虽然简单,但在生产环境中是致命的。shell_exec 是同步的,如果用户请求生成一个包含 1000 个公式的 PDF,PHP 可能会在这个命令上挂起 10 秒甚至更久,直到超时。这就是我们要在后面解决的问题。
第三回:数据清洗与格式化——PHP 的脏活
LaTeX 的强大在于排版,但它的输入必须是极其干净的。PHP 的数据往往充满了“人性的弱点”:多余的空格、科学计数法、不规范的数组。
我们的任务是把 PHP 的脏数据,清洗成 LaTeX 的“圣水”。
1. 科学计数法与数字格式化
LaTeX 很讨厌科学计数法。你不能直接写 $1.23e-5。你需要把它写成 num{1.23 times 10^{-5}}。
我们需要一个专门的 Formatter 类。
class MathFormatter {
// 将 PHP 的科学计数法数字转换为 LaTeX 的 num{} 格式
public static function formatNumber($number, $precision = 4) {
if (is_float($number) && abs($number) < 0.0001 && abs($number) > 0) {
return sprintf('\num{%.{$precision}f \times 10^{%d}}', $number, floor(log10(abs($number))));
}
// 处理千分位
if (is_numeric($number)) {
return number_format($number, $precision, '.', '');
}
return (string)$number;
}
// 处理数组转矩阵
public static function arrayToMatrix(array $data) {
$rows = count($data);
$cols = count($data[0]);
$latex = "\begin{pmatrix}n";
foreach ($data as $row) {
$rowStr = array_map([$this, 'formatNumber'], $row);
$latex .= implode(' & ', $rowStr) . " \\n";
}
$latex .= "\end{pmatrix}";
return $latex;
}
}
// 测试数据
$mathData = [
[1000000.5, 0.00000012],
[5000, 123.456]
];
echo MathFormatter::arrayToMatrix($mathData);
输出结果将是:
begin{pmatrix}
1000000.5 & 0.0000 \
5000 & 123.456
end{pmatrix}
你看,这里 PHP 负责了所有的逻辑判断和数值格式化,把复杂的格式化交给 PHP 的 number_format 和 log10,最后输出给 LaTeX。
2. 处理中文(痛并快乐着)
如果你需要输出中文文档,必须引入 ctex 宏包。这就像是给 LaTeX 买了一副中文听诊器。
在你的 .tex 模板头部,一定要有这行:
documentclass{article}
usepackage[UTF8]{ctex} % 引入中文支持
usepackage{amsmath} % 数学公式支持
usepackage{amssymb} % 数学符号支持
第四回:动态公式生成引擎——让 PHP 成为数学家
这是最酷的部分。很多业务系统需要根据用户的选择,动态生成复杂的数学公式。比如一个“折旧计算器”,用户选了“直线折旧法”,我们就生成 $sum$ 符号;选了“双倍余额递减法”,我们就生成复杂的积分形式。
我们可以构建一个公式构建器。
代码示例:动态公式生成
class FormulaBuilder {
public static function calculateDepreciation($method, $cost, $salvage, $life) {
switch ($method) {
case 'straight':
// 直线法: (成本 - 残值) / 年限
return sprintf(
"直线折旧法公式: \frac{%s - %s}{%s} = \num{%s}",
MathFormatter::formatNumber($cost),
MathFormatter::formatNumber($salvage),
MathFormatter::formatNumber($life),
MathFormatter::formatNumber(($cost - $salvage) / $life)
);
case 'double':
// 双倍余额递减法: 比较复杂的逻辑,这里简化演示
// 公式:年折旧率 = 2 / 使用年限 * 年初账面净值
return sprintf(
"双倍余额递减法: 年折旧率 = \frac{2}{%s} \times (成本 - 累计折旧)",
MathFormatter::formatNumber($life)
);
default:
return "未知方法";
}
}
// 生成积分公式示例
public static function generateIntegral($func, $a, $b) {
return sprintf(
"\int_{%s}^{%s} %s \, dx",
MathFormatter::formatNumber($a),
MathFormatter::formatNumber($b),
$func
);
}
}
然后在 PHP 循环中,我们根据数据库查询结果,动态拼接 LaTeX 代码块。
$reportData = [
['asset' => '服务器', 'method' => 'straight', 'cost' => 5000, 'salvage' => 100, 'life' => 5],
['asset' => '显卡', 'method' => 'double', 'cost' => 2000, 'salvage' => 0, 'life' => 3],
];
$html = "<ul>";
foreach ($reportData as $item) {
$formula = FormulaBuilder::calculateDepreciation(
$item['method'],
$item['cost'],
$item['salvage'],
$item['life']
);
$html .= "<li>资产 {$item['asset']}:$formula</li>";
}
$html .= "</ul>";
这里 PHP 的灵活性展现得淋漓尽致。我们可以把公式内容存在数据库里,甚至让前端用户自己写公式字符串(当然这有安全风险,需要转义)。
第五回:布局的暴政——表格与 TikZ
PHP 处理二维数组很顺手,但要把二维数组塞进 LaTeX 表格,那是一场噩梦。表格太宽了会跑版,太窄了会难看。
我们需要一个 TableRenderer。
class TableRenderer {
public static function render(array $data, $caption = "数据表") {
$cols = count($data[0]);
$rows = count($data);
// 1. 构建表格头部
$header = array_keys($data[0]);
$latexHeader = " \toprule n";
$latexHeader .= " " . implode(" & ", $header) . " \\n";
$latexHeader .= " \midrule n";
// 2. 构建表格内容
$body = "";
foreach ($data as $row) {
// PHP 数组索引从 0 开始,LaTeX 不喜欢,得处理一下
// 这里假设数据已经是清洗好的,如果是原始数据,需要 array_values
$cleanRow = array_values($row);
$body .= " " . implode(" & ", $cleanRow) . " \\n";
}
$body .= " \bottomrule n";
return "\begin{table}[h]n" .
"\centeringn" .
"\caption{$caption}n" .
"\begin{tabular}{" . str_repeat('c|', $cols-1) . 'c}' . "n" .
$latexHeader . $body . "n" .
"\end{tabular}n" .
"\end{table}";
}
}
但这只是静态的表格。如果你需要那种“轴标签”清晰、网格线漂亮的图表,LaTeX 有个神器叫 TikZ。它是一个基于代码的绘图引擎。
如果你觉得 PHP 生成图片很慢(GD 库确实慢),那就让 LaTeX 画。
代码示例:用 PHP 生成 TikZ 代码
public static function renderChart($labels, $values) {
$maxVal = max($values);
$points = "";
$width = 10; // 图表宽度 10cm
foreach ($values as $i => $val) {
$x = $i * 2 + 1; // 每个柱子宽 2cm
$h = ($val / $maxVal) * 8; // 高度比例
$points .= sprintf("\draw[fill=blue!30] (%s,0) rectangle (%s,%s);n", $x-1, $x, $h);
$points .= sprintf("\node at (%s,%s) [below] {%s};n", $x, 0, $labels[$i]);
}
return "\begin{tikzpicture}[scale=0.8]n" .
"\draw (0,0) grid (12,9);n" . // 网格线
$points .
"\end{tikzpicture}";
}
注意: TikZ 渲染非常慢。如果你的数据量大,每生成一个柱子都要编译一次 PDF,你的服务器会死机的。建议批量生成。
第六回:生产环境的噩梦与救赎——异步与缓存
现在,理论讲完了。让我们面对现实。生产环境是残酷的。
1. 同步阻塞的陷阱
还记得我们第一回写的 shell_exec 吗?如果用户请求生成一份包含 50 页、每页都有复杂公式的工程计算书,LaTeX 可能需要 30 秒甚至 1 分钟。
在 PHP 的生命周期里,这意味着:
- 浏览器转圈圈(用户体验极差)。
- PHP 进程被占用(其他请求进不来)。
- 内存溢出(如果编译器产生大量临时文件)。
解决方案:队列系统。
我们需要把“生成 PDF”这个任务扔进队列(RabbitMQ, Redis, Kafka)。
- 用户点击“下载报告”。
- PHP 立即返回一个“任务 ID”给用户,显示“正在计算中…”。
- 后台有一个 PHP Worker 进程(通过 Supervisor 守护进程)一直在监听队列。
- Worker 拿到任务 ID,调用
LatexEngine,生成 PDF,存到 S3 或本地磁盘。 - Worker 通知前端:任务完成,可以下载了。
2. 缓存的艺术
PDF 是昂贵的资源。同一个报告,周一生成了一次,周二用户又想下载,你难道要再让 LaTeX 编译一遍吗?
我们需要哈希。把数据里的每一个关键字段拼接起来,做 MD5。
function getReportHash($data) {
return md5(serialize($data) . $data['template_version']);
}
// 伪代码逻辑
$hash = getReportHash($reportData);
$cachePath = "/var/www/cache/pdfs/{$hash}.pdf";
if (!file_exists($cachePath)) {
// 生成 PDF 并保存到 $cachePath
$engine->compile($reportData);
}
// 直接返回 $cachePath
第七回:实战案例——动态生成技术手册
假设我们要为一个物联网设备生成技术手册。设备参数在数据库里,但手册必须是标准化的、带图标的、带公式的。
数据结构:
[
{
"section": "电气参数",
"items": [
{"label": "工作电压", "value": 220, "unit": "V"},
{"label": "工作电流", "value": 5.2, "unit": "A"}
]
},
{
"section": "通信协议",
"formulas": [
{"type": "integral", "func": "I(t)", "a": 0, "b": 10}
]
}
]
PHP 处理流程:
function generateTechManual($dbData) {
$texContent = "\documentclass{article}n";
$texContent .= "\usepackage{ctex}n";
$texContent .= "\usepackage{graphicx}n";
$texContent .= "\usepackage{amsmath}n";
$texContent .= "\title{技术规格说明书}n";
$texContent .= "\author{PHP 自动生成}n";
$texContent .= "\begin{document}n";
$texContent .= "\maketitlenn";
foreach ($dbData as $section) {
$texContent .= "\section*{" . $section['section'] . "}n";
if (isset($section['items'])) {
$texContent .= TableRenderer::render($section['items'], "参数列表");
$texContent .= "nn";
}
if (isset($section['formulas'])) {
foreach ($section['formulas'] as $formula) {
$texContent .= FormulaBuilder::generateIntegral(
$formula['func'],
$formula['a'],
$formula['b']
) . " \nn";
}
}
}
$texContent .= "\end{document}";
return $texContent;
}
// 运行
$manualContent = generateTechManual($dbData);
file_put_contents('/tmp/manual.tex', $manualContent);
看,短短几十行 PHP 代码,我们就构建了一个结构化、模块化的 LaTeX 文档生成器。这就是“业务逻辑”到“高精度排版”的桥梁。
第八回:常见坑与排雷指南
在这个领域,你会遇到很多坑,别哭,我们一个个过。
坑 1:字符编码
LaTeX 源文件最好使用 UTF-8 编码。如果你用 file_put_contents 写入中文,确保不要搞错编码。在 Windows 上,fwrite 默认可能不是 UTF-8,记得 BOM 的问题。
坑 2:循环依赖与包加载
LaTeX 有个毛病,就是如果你引用了 graphicx,它就得去找图片。如果你引用了 amsmath,它得找数学包。如果你的模板里引入了太多不常用的包,编译速度会慢得像蜗牛。请保持模板的“极简主义”。
坑 3:错误信息
LaTeX 的错误信息非常抽象。
! Missing $ inserted. <inserted text> $
这通常意味着你在 text{} 里面用了数学符号。PHP 调试是 Undefined variable,LaTeX 调试是上帝在跟你说话。
坑 4:字体缺失
在服务器上生成 PDF,报错 Font T1/cmr10 not found。这是因为服务器上没有安装特定的字体(比如数学字体)。在服务器上尽量使用标准字体,或者指定字体文件路径。
第九回:进阶玩法——变量替换与宏包
为了更方便地管理 LaTeX 模板,我们可以使用类似于 Markdown 的语法,或者 PHP 的 Twig 模板引擎。
假设我们有一个 report.tex 模板:
documentclass{a4paper,12pt}
usepackage{ctex}
title{{{ title }}}}
begin{document}
maketitle
section{摘要}
{{ summary }}
section{详细数据}
{{ table_content }}
section{结论}
这里是根据数据自动生成的结论:{{ conclusion }}
end{document}
然后在 PHP 里用 Twig 渲染它:
$loader = new TwigLoaderFilesystemLoader('/path/to/templates');
$twig = new TwigEnvironment($loader);
$template = $twig->load('report.tex');
$context = [
'title' => '2023年度财务分析',
'summary' => 'PHP 生成的摘要...',
'table_content' => TableRenderer::render($data),
'conclusion' => '综上所述,该项目具有极高的可行性。'
];
$output = $template->render($context);
这样,你就把 PHP 的数据驱动能力和 LaTeX 的排版能力完美结合了。你甚至可以把 .tex 模板放在 Git 仓库里,让设计人员(或者更懂排版的人)去维护模板,而开发人员只负责数据。
第十回:总结——拥抱混乱的秩序
好了,朋友们。
我们今天谈了 PHP,谈了 LaTeX,谈了队列,谈了 TikZ。
这其实是一场关于“控制权”的斗争。
传统的 PHP 生成 HTML,是把控制权交给浏览器。浏览器很懒,它只想把文字塞进去,管它美不美。
而 LaTeX 是个控制狂,它要求你每一个空格、每一个换行、每一个公式的间距都要精确。
当你用 PHP 去调用 LaTeX 时,你实际上是在做一件很酷的事情:你在服务器端掌控了最终的呈现形式。
不要害怕 ! Missing $ inserted 的报错。不要害怕编译慢。当你看到那个生成的 PDF,行间距完美,数学公式直立,甚至比你在 Word 里手动排版了一整天还要好时,你会觉得这一切都是值得的。
哪怕只是为了能用一行 PHP 代码打印出一个完美的积分符号,这趟旅程也值了。
现在,回去你的服务器上,安装 TeX Live,写好你的第一个 PHP 脚本,开始生成你的第一份完美文档吧。别告诉我你还没安装好环境哦。
(注:安装 TeX Live 下载文件巨大,建议使用国内镜像源,比如清华源,否则你会因为下载 4GB 的包而怀疑人生。)
Happy Coding, Happy Typesetting!