为什么你的 PDF 引擎该待在 PHP 里,而不是一个边车里
Spec: ISO/IEC 25010:2023, §3.7ISO/IEC 25010:2023 §3.7Spec: ISO 32000-2, §7ISO 32000-2 §7
一份 PDF 可以在两个地方被生成:在你的 PHP 进程内部,或者在你不得不去运维的别处。NextPDF 在内部生成它。本页论证的正是这个选择——为什么一个进程内引擎通常是正确的默认值,以及那个“别处”模式一旦进入生产环境,实际究竟要花多少代价。
这是架构的角度,而不是框架的角度。同一个引擎如何触达 Laravel、Symfony、CodeIgniter 与独立运行代码,则是另一个故事,在一个引擎,适配每一种框架中讲述。
为何重要
标题为“为何重要”的章节一项 PDF 功能很少一开始就是一个你要运维的系统。它始于控制器里的一行代码:渲染这张发票、返回那份报告。而边车模式把那一行变成了基础设施。为了画出这份文档,你如今要运行第二样东西——一个外部二进制程序、一个无头浏览器、一个独立的微服务——而那第二样东西所需要的一切,也都成了你的问题:它的版本、它的内存、它的容器、它的网络、它的失败模式、它在凌晨两点的那一通值班呼叫。
这成本在演示时不可见,在生产环境中却无可回避。一个活在你进程里的文档引擎,一样都没有。问题不是“一个边车能不能生成一份 PDF”——它当然能。问题是“为了走到那一步,你签下了要去运维什么,以及你是否真的需要它”。
精简版说明
标题为“精简版说明”的章节- 进程内意味着没有第二个运行时。 NextPDF 在处理该请求的那同一个 PHP 工作进程内部画出这份 PDF。没有需要派生的子进程,没有需要部署的服务,也没有任何额外的东西需要保活。
- 一个边车添上了一片你原本没有的运维面。 一个捆绑的浏览器或外部二进制程序,会带来它自己的版本、它自己的安全足迹,以及它自己的容器——所有这些如今都由你来打补丁与监控。
- 进程的边界正是出错的地方。 冷启动、超时、脆弱的进程间管道,以及数据离开你的进程,都是失败模式,而一次进程内调用根本就不会有这些。
- 进程内是可测试且确定性的。 这个引擎是你能做单元测试、能做模拟、能加以推理的类型化 PHP——而不是一个你只能靠运行它、看其输出来探测的不透明渲染器。
- 一个真正的浏览器仍然有其真正的用武之地。 对任意现代网页进行像素级精确的渲染,无头浏览器是那个诚实的工具——而 NextPDF 可以刻意地委派给它。它是一道接缝,而不是默认值。
NextPDF 的处理方式
标题为“NextPDF 的处理方式”的章节把这两种架构并排放在一起看。进程内的那条路径是一次函数调用。边车那条路径则是一个微缩的分布式系统——而它各个方框之间的每一支箭头,都是一个独立于你的代码而失败的地方。
- In-process: call the enginewriteHtml() or the document API runs inside the current PHP worker — no subprocess, no socket.
- In-process: receive PDF bytesThe engine returns native PDF content directly; nothing left the process.
- Sidecar: serialize and shipMarkup or a request is marshalled out of your process to a binary, browser, or remote service.
- Sidecar: cross the boundaryA process spawn or network hop — with a cold start, a timeout, and an IPC contract that can break.
- Sidecar: run a second runtimeAn external renderer with its own version, memory profile, and security surface to operate and patch.
- Sidecar: deserialize backMarshal the result back in and translate the renderer’s errors into yours.
没有第二个运行时要运维。 边车模式是两个系统披着一项功能的戏服。一个捆绑的 wkhtmltopdf、一个无头 Chromium 服务、一个独立的渲染微服务——每一个都是一个有其自身发布节奏、有其自身缺陷的运行时。你把它们全部继承下来。进程内引擎以一个 Composer 依赖的形式发运;它的升级方式跟你 composer.json 里每一个其他库一样,不会给你的部署添上任何守护进程、镜像或套接字。
版本漂移与一片更宽的安全面。 一个捆绑的浏览器是一个庞大的、快速演进的代码库,伴有源源不断的安全公告。固定它,它会腐烂;追随它,它会动荡。无论哪一种,那都是一整套渲染器的 Web 平台坐镇在你的供应链里,只为喂养一份文档。一个进程内 PHP 引擎是一个聚焦的、你能读懂的代码库;它的安全面就是你本就在运行的那套 PHP,而不是一个你如今还要额外运行的第二平台。
数据留在你的进程边界之内。 当你外壳调用出去时,文档内容——而它往往恰恰就是一份 PDF 之所以存在所要承载的那份敏感数据——会越过一道边界。它会被写入一个管道、一个参数、一个临时文件,或一个通往某个服务的网络套接字。这些当中的每一个,都是一处可能泄漏、可能被无意记录、或可能被遗留下来的地方。在进程内,数据从不离开那个拥有它的工作进程。爆炸半径是一个进程,而不是一支机群。
脆弱的管道、冷启动与超时。 进程间调用与网络调用会以一次函数调用不可能的方式失败:那个没能启动的子进程、那个挂住的套接字、那个你猜错了的超时、那次流量高峰下的冷启动。每一个都需要一套重试策略、一个断路器,以及一份预算。一次进程内渲染要么返回字节,要么抛出一个你在下一行就能捕获的类型化异常。没有任何残留的网络状态需要去对账。
可观测性与测试会因为跨越那道边界而变难。 一次边车里的失败,是以一个退出码、一行被截断的日志,或一个来自某个你并不掌控的服务的 500 的形式抵达你的。要复现它,就意味着复现整个那个环境。一个进程内引擎用你本就在用的工具就可观测——一份堆栈跟踪、一个调试器、一个性能分析器——而且它的可测试方式跟你其余的 PHP 一样。这种可测试性是一项有名有姓的软件质量属性:ISO/IEC 25010 把它归在可维护性之下(Spec: ISO/IEC 25010:2023, §3.7ISO/IEC 25010:2023 §3.7),而一个进程内库满足它的方式,远比一个你只能靠启动它来检验的渲染器更为直接。
那些测试所断言的对象——那份 PDF——是一个有定义的结构,而不是一个黑盒。一个 PDF 文件有一套被规定好的对象与文件布局(Spec: ISO 32000-2, §7ISO 32000-2 §7),而一个进程内引擎是从你能读懂的代码里发出那个结构的——于是一个黄金文件测试或结构性测试,核查的是一个已知函数所产出的字节,而不是一个你只能旁观的外部程序的输出。
实务范例
标题为“实务范例”的章节整个要点用寥寥数行就装得下。没有客户端、没有基础 URL、没有健康检查、也没有重试策略——因为根本就没有第二个系统。
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
// The engine runs inside this very process. No subprocess is spawned,// no socket is opened, and the report data never leaves the worker.$document = Document::createStandalone();$document->setTitle('Quarterly Report');$document->addPage();
$html = <<<'HTML'<h1 style="color: #1E3A8A;">Quarterly Report</h1><p>Rendered <strong>in-process</strong> by PHP — no browser, no sidecar.</p>HTML;
$document->writeHtml($html);
// PDF bytes are returned directly. There is no boundary to marshal across,// so there is no timeout, cold start, or deserialization step to handle.$bytes = $document->getPdfData();对照一下边车版本的形态——不是它的代码,而是它的运维形态。它需要一个二进制程序或服务被安装妥当且可达、一个请求被序列化并发出、一个被选定的超时、一条在渲染器冷掉或宕掉时的失败路径,以及把结果整理回来。上面那段代码片段里一样都没有,因为当引擎是一个库时,这些一样都不存在。
常见误解
标题为“常见误解”的章节那个常见的假设是:“真正的”PDF 渲染必定意味着一个浏览器,所以进程内必定是那个玩具版本。这把权衡弄反了。当你需要对任意现代 Web 内容进行精确的、像素级精确的渲染时,浏览器才是那个正确的工具。而对于大多数团队实际所做的那类文档形态的工作——发票、报告、对账单、合同——其中布局是已知的、数据是你自己的、正确性由一个验证器而非肉眼来核查,浏览器则是个错误的默认值。对于那类工作,一个边车的运维重量买不到任何进程内引擎尚未给你的东西,却让你在上面那几节里付出一切。
与之相对的误解,正是本页小心翼翼不去犯的那一个:声称一个进程内引擎能像浏览器那样渲染“整个 Web”。它并不能,而 NextPDF 也不假装它能。它的进程内 HTML 管线是一个与规范对齐的、聚焦于文档布局的子集,带有被记录在案的边界——那份诚实的范围都摆在HTML 管线里。当你确实需要完整的浏览器保真度时,那是一次刻意的、需主动选择加入的委派,而不是一次悄无声息的回退。
限制与边界
标题为“限制与边界”的章节进程内是正确的默认值。这并不是一个普适的主张,说一个子进程永远没有正当理由。当一份文档确实要求对进程内引擎并不覆盖的任意现代 CSS 进行精确渲染时,委派给一个无头浏览器就是正确的选择——而 NextPDF 刻意地支持那条路径,并约束其网络访问,作为一道接缝,而不是默认值。这两者不是对手;它们是面向不同任务的不同工具。
本页论证的是架构,而不是一张 CSS 支持矩阵。进程内管线究竟覆盖哪些 HTML 与 CSS,是由引擎的代码及其符合性测试来定义的,并随那条管线一同被记录在案——而不是在这里许下承诺。“进程内”描述的是默认的渲染路径;它不是一个主张,说每一条可能的路径都避开了子进程。
能力面保持简单:进程内引擎属于 Core,而浏览器委派路径是一项可选的扩展,独立于版本。
| Edition | Availability |
|---|---|
| Core | Core renders PDF in-process in PHP — no subprocess, binary, or sidecar by default. |
| Pro | The headless-browser delegation path is an optional add-on extension, independent of edition tier. |
| Enterprise | The headless-browser delegation path is an optional add-on extension, independent of edition tier. |
相关文档
标题为“相关文档”的章节- HTML 管线 —— 进程内引擎那份诚实的范围,以及究竟何时委派给一个浏览器才是正确的。
- 一个引擎,适配每一种框架 —— 那条互补的轴线:同一个进程内引擎如何触达每一种 PHP 框架,而无需为每套技术栈配一个不同的库。
- 在生产环境中运维 NextPDF —— 运行一个进程内引擎日常看起来是什么样的,没有额外的运行时要运维。
- 内存与流式处理 —— 引擎如何在负载之下让进程内生成保持有界。
词汇表
标题为“词汇表”的章节- 进程内生成(In-process generation) —— 在处理该请求的那同一个 PHP 工作进程内部生成这份 PDF,没有子进程、套接字或外部服务。
- 边车(Sidecar) —— 一个与你的应用程序并行运行、只做一件事的独立运行时;在此指一个在你进程之外渲染这份 PDF 的外部二进制程序、无头浏览器或微服务。
- 冷启动(Cold start) —— 当一个子进程或服务必须从零启动、然后才能服务第一个请求时所招致的延迟与资源尖峰。
- IPC —— 进程间通信:用来把数据传入和传出一个独立进程的管道、套接字、临时文件或网络调用,也是一个反复出现的、脆弱且难以调试的失败来源。
- 浏览器委派接缝(Browser-delegation seam) —— 那条可选的、需主动选择加入的路径,把一次渲染交给一个无头浏览器以求精确的保真度,并阻断子资源的网络访问;这是一个刻意的选择,而不是默认值。