使用 Gotenberg 将 Office 文档转换为 PDF
Gotenberg 桥接会将一份 Office 文档转换为 PDF。它会通过 HTTPS 将文档发送到 Gotenberg 微服务,并返回 PDF 字节。先使用不可变的 GotenbergConfig 描述服务,将一个 PSR-18 客户端与 PSR-17 工厂接入 GotenbergBridge,通过健康检查探测服务,然后转换磁盘上的文件或内存中的字节。本指南涵盖基于扩展名的格式检测、健康探测、类型化失败合约,以及交接给 NextPDF 后处理的流程。
先讲清楚的前置条件:
- 已安装 NextPDF 核心与
nextpdf/gotenberg。 - 有一个可通过 HTTPS 连接的 Gotenberg 服务。在任何请求离开进程之前,桥接就会拒绝单纯的
http://URL。 - 已安装一个 PSR-18 客户端,以及 PSR-17 的 request 与 stream 工厂。若要做 DNS 与 TLS 固定,你还需要提供一个 PSR-17 response 工厂。
- 输入是六种已识别的 Office 格式之一:
.docx、.xlsx、.pptx、.odt、.ods或.odp。桥接会以ValueError拒绝任何其他扩展名。
这是一份操作指南。如需一个可直接运行的完整程序,请阅读 Gotenberg 快速入门。
安装桥接、一个 PSR-18 客户端,以及 PSR-17 工厂。
composer require nextpdf/gotenberg guzzlehttp/guzzle运行一个可通过 HTTPS 连接的 Gotenberg 服务,并从密钥管理器或注入的环境值中获取任何 bearer token。桥接从不读取环境变量,也不会自行构建 HTTP 客户端;这两者都由你提供。
概念说明
标题为“概念说明”的章节GotenbergBridge::convertFile() 接受一个磁盘路径。它会规范化路径以阻挡路径穿越,将扩展名映射到支持的格式,筛查大小与文件名,再将一个 multipart 请求发送到 <apiUrl>/forms/libreoffice/convert。convertString() 会对你已有的字节执行同样的操作;它会使用原始文件名来检测扩展名。
格式检测基于扩展名完成。桥接会将 .docx、.xlsx、.pptx、.odt、.ods 与 .odp 映射到各自的格式,并在任何网络流量发生之前就以 ValueError 拒绝其他任何格式。结果对象会通过枚举值公开检测到的来源格式。
桥接本质上是一次带验证的同步 HTTP 往返。它不会重试、排队、缓存或限流;这些都属于桥接外围的应用程序职责。请将每次转换都视为一次发往外部服务的远程调用:该服务由你运营,但无法在进程内掌控,因此需要围绕它的延迟与失败进行设计。
桥接会通过类型化异常暴露失败,绝不返回部分结果或未经验证的结果:
- 非
200状态、Content-Type(其值不含application/pdf),或开头不是%PDF的内容,都会分别引发GotenbergConvertException。只有当这三项检查全部通过时,桥接才会返回结果。 - PSR-18 客户端失败(包括网络失败或超时)会被包装为
GotenbergConvertException,并以原始异常作为成因。 - 验证失败(非 HTTPS URL、私有或保留地址、输入过大、不安全的文件名)会在任何网络流量发生之前就引发
RuntimeException。 - 无法识别的文件扩展名会在任何网络流量发生之前就引发
ValueError。
API 接口
标题为“API 接口”的章节// Configuration (final readonly):new GotenbergConfig( string $apiUrl, // required, must be HTTPS int $timeout = 30, // hard transfer timeout, seconds int $maxFileSize = 52_428_800, // 50 MiB string $apiKey = '', // #[SensitiveParameter]; Bearer when non-empty list<string> $pinnedPublicKeys = [], // sha256/<base64> list<string> $backupPublicKeys = [],)GotenbergConfig::fromArray(array $config): selfGotenbergConfig::isValid(): bool
// The bridge:new GotenbergBridge( GotenbergConfig $config, ClientInterface $httpClient, // PSR-18 RequestFactoryInterface $requestFactory, // PSR-17 StreamFactoryInterface $streamFactory, // PSR-17 ?LoggerInterface $logger = null, // PSR-3 ?HtmlSecurityPolicyInterface $htmlSecurityPolicy = null, ?ResponseFactoryInterface $responseFactory = null, // enables pinned transport)GotenbergBridge::isAvailable(): boolGotenbergBridge::convertFile(string $path): GotenbergConvertResultGotenbergBridge::convertString(string $bytes, string $originalFilename): GotenbergConvertResult结果对象会公开 pdfData、sourceFormat 枚举、isValid()(当内容非空且开头为 %PDF 时为 true),以及 size()。完整的字段参考、fromArray() 键映射,以及传输选择规则,请见“另请参阅”中链接的 Gotenberg 配置页面。
代码示例 — 快速开始
标题为“代码示例 — 快速开始”的章节描述服务、接入桥接、探测服务,然后转换一个文件。
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Gotenberg\GotenbergBridge;use NextPDF\Gotenberg\GotenbergConfig;use NextPDF\Gotenberg\GotenbergConvertException;
$config = new GotenbergConfig( apiUrl: 'https://gotenberg.example.com', timeout: 60, apiKey: getenv('GOTENBERG_TOKEN') ?: '',);
$bridge = new GotenbergBridge( config: $config, httpClient: $httpClient, // your PSR-18 client requestFactory: $requestFactory, // your PSR-17 factory streamFactory: $streamFactory, // your PSR-17 factory responseFactory: $responseFactory, // enables the pinned transport);
// Probe before converting. The probe validates the URL with no network// traffic, then sends a HEAD to <apiUrl>/health.if (!$bridge->isAvailable()) { throw new RuntimeException('Gotenberg is not reachable.');}
try { $result = $bridge->convertFile('/path/to/report.docx');} catch (GotenbergConvertException $exception) { // Bad config, HTTP failure, non-200, wrong Content-Type, or non-PDF body. throw $exception;}
if (!$result->isValid()) { throw new RuntimeException('Result is not a valid PDF.');}
file_put_contents('/path/to/report.pdf', $result->pdfData);这个类是 NextPDF\Gotenberg\GotenbergConfig(上面那行用的正是你的代码必须导入的命名空间)。isAvailable() 对于空的、非 HTTPS 或私有地址的 URL,以及任何网络错误,都会返回 false,从不抛出异常;只要返回低于 500 的状态(来自 /health),即表示可用。
代码示例 — 生产环境
标题为“代码示例 — 生产环境”的章节生产环境的转换应分别捕获每一种失败类型,只在正确条件下重试,并在调用端一侧限制并发量。下面的 catch 顺序是完整穷举的。
<?php
declare(strict_types=1);
use NextPDF\Gotenberg\GotenbergBridge;use NextPDF\Gotenberg\GotenbergConvertException;use Psr\Log\LoggerInterface;use RuntimeException;use ValueError;
final readonly class OfficeConverter{ public function __construct( private GotenbergBridge $bridge, private LoggerInterface $logger, ) {}
public function convert(string $path): string { try { $result = $this->bridge->convertFile($path); } catch (GotenbergConvertException $exception) { // Transport, non-200, wrong Content-Type, or non-PDF body. // Retry only on transport-level or 502/503/504 causes, with // bounded exponential backoff and jitter — never blind retries. $this->logger->error('gotenberg.convert.failed', [ 'path' => basename($path), 'exception' => $exception::class, ]); throw $exception; } catch (ValueError $exception) { // Extension is not one of the six recognized Office formats. $this->logger->warning('gotenberg.convert.unsupported_format', [ 'path' => basename($path), ]); throw $exception; } catch (RuntimeException $exception) { // Non-HTTPS URL, private address, oversized input, or unsafe name. $this->logger->error('gotenberg.convert.rejected', [ 'path' => basename($path), 'exception' => $exception::class, ]); throw $exception; }
if (!$result->isValid()) { throw new RuntimeException('Gotenberg returned an invalid PDF body.'); }
return $result->pdfData; }}只在传输层的 GotenbergConvertException(一个被包装的 PSR-18 客户端异常),以及幂等的服务器错误(502、503、504)时重试。400 级别的响应通常表示输入有误,因此重试也会以同样方式失败。限制总尝试次数与总实际耗时。将同时进行的转换数量限制在你的 Gotenberg 部署可承受的容量内。桥接本身是无状态的,可安全地从多个 worker 使用,但服务的转换容量是有限的。
边界情况与陷阱
标题为“边界情况与陷阱”的章节- 格式检测基于扩展名判断。 一个
.docx被改名为.txt会被以ValueError拒绝;而一个.txt被改名为.docx则会被发送到 Gotenberg,并在那里失败。接受上传时,要信任真正的格式,而不是文件名。 fromArray()在设计上是宽容的。 它会对格式错误的输入静默代入默认值。在你的启动流程中验证来源数组,让缺少的 URL 尽早以配置错误的形式浮现,而不是变成每次转换时才抛出的异常。- 大小上限是在进程内强制执行的。
maxFileSize(默认 50 MiB)会在请求发送之前就被检查,因此过大的文件永远不会消耗服务容量。将上限降到刚好满足你的文档所需;较小的上限是成本更低的拒绝服务防护。 - 探测不是免费的。 请从就绪或健康检查端点调用
isAvailable(),而不是在每次转换前都调用。每次转换都跑一遍探测,会让你对服务的请求率加倍,却毫无收益。 - 进程内不做缓存。 如果同一份文档会被反复转换,请在你的应用程序中以输入内容哈希为键,缓存产出的 PDF。
renderTimeMs由你自己设定。 除非你的集成已经测量并设定它,否则结果的计时字段会是0.0。如果你需要这个数字,请自行给这次调用计时。
在请求期间,一次转换会在 Gotenberg 侧占用一条连接与一个 LibreOffice worker,而 Office 转换并非瞬间完成。请根据你的真实文档测得的转换延迟来设置 timeout,并留出余量。将它设得低于任何上游网关或 PHP max_execution_time,这样桥接会先超时,你会得到一个类型化异常,而不是一个被强制终止的进程。使用队列、信号量,或规模与服务容量匹配的 worker 池来限制并发量。进程内没有缓存;如果你会反复转换同一份输入,请在你的应用程序中加上一层缓存。
安全须知
标题为“安全须知”的章节- 发送前先做 HTTPS 与地址筛查。 在任何请求离开进程之前,桥接就会拒绝非 HTTPS URL,以及会 resolve(解析)到私有或保留地址空间的目的地。每次重试的调用都会重新执行这套验证,因此重试无法绕过 SSRF 防护。
- 按需采用固定传输。 当你提供 response 工厂与固定(或已有一组解析后的 IP)时,桥接会将连接绑定到解析后的地址、强制执行 SPKI 固定、验证对端与主机、应用超时,并停用跟随重定向。证书轮替之前,请先设定好一个备用固定。
- 不要信任上传文件所声明的内容类型。 接受用户上传时,请自行验证真正的文件类型;扩展名映射到格式只是一个路由决策,不是真伪性检查。
- 密钥会被遮蔽且不可变。
apiKey带有#[SensitiveParameter],而配置是final readonly。请从密钥管理器获取 token;绝不要把它提交进版本库。记录下来的转换条目会带有 URL、文件名、格式与内容长度 —— 绝不会包含文件内容或 token。 - 绝不要写空的
catch块。 每个示例都会捕获特定类型,并带上下文记录下来。
完整的安全与部署模型,请见 Gotenberg 安全与运维页面。PSR-18 传输合约,以及“不要信任内容类型”的指引,都固定到上游生产使用页面上各自的条款。
符合性
标题为“符合性”的章节本指南本身不提出任何规范性的标准主张。桥接的 PSR-18 传输行为(客户端只在无法发送请求或无法解析响应时才引发异常;4xx/5xx 是正常的返回值)、文件上传验证指引,以及 TLS 固定模型,都固定到上游 Gotenberg 生产使用与配置页面上的 PSR-18、OWASP 与 RFC 7469。这份 cookbook 页面只重述用法,并将这些引用交由那些页面处理。桥接会产出 PDF 字节,然后就停下来。签名、PDF/A 配置文件与水印,属于 NextPDF 后处理的范畴,也是商业版能力,并非这个桥接的一部分。
另请参阅
标题为“另请参阅”的章节- 从控制器返回生成的 PDF —— 把转换后的 PDF 当成 HTTP 响应返回。
- Gotenberg 快速入门 —— 完整可运行的转换程序。
- Gotenberg 配置 —— 每个字段、
fromArray()映射,以及传输选择。 - Gotenberg 生产使用 —— 密钥、超时、重试、并发量,以及后处理界线。