跳到內容

使用 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 工廠。

Terminal window
composer require nextpdf/gotenberg guzzlehttp/guzzle

啟動一個可透過 HTTPS 存取的 Gotenberg 服務,並從祕密管理器或注入的環境值取得任何 bearer token。橋接從不讀取環境變數,也不會自行建立 HTTP 用戶端;這兩者都由你提供。

GotenbergBridge::convertFile() 接受磁碟上的路徑。它會正規化路徑以阻擋路徑穿越,把副檔名對映到支援的格式,檢查大小與檔名,再把一個 multipart 請求送到 <apiUrl>/forms/libreoffice/convertconvertString() 會對你手上已有的位元組執行同樣流程;它會使用原始檔名來偵測副檔名。

格式偵測是依副檔名判斷。橋接會把 .docx.xlsx.pptx.odt.ods.odp 對映到各自的格式,並在任何網路流量之前就以 ValueError 拒絕其他任何格式。結果物件會以列舉值揭露偵測到的來源格式。

橋接本質上是一次同步的 HTTP 往返,外層加上驗證。它不會重試、排隊、快取或限流;那些責任屬於橋接外圍的應用程式。請把每次轉換都視為對一個由你營運、但無法在行程內掌控的服務發出的遠端呼叫,並針對它的延遲與失敗來設計。

橋接會以型別化的例外揭露失敗,絕不回傳部分結果或未經驗證的結果:

  • 200 狀態、Content-Type(其值不含 application/pdf)不符,或內容開頭不是 %PDF,都會各自引發 GotenbergConvertException。只有當這三項檢查全部通過時,橋接才會回傳結果。
  • PSR-18 用戶端失敗(包括網路失敗或逾時)會被包裝成 GotenbergConvertException,並以原始例外作為成因。
  • 驗證失敗(非 HTTPS URL、私有或保留位址、輸入過大、不安全的檔名)會在任何網路流量之前就引發 RuntimeException
  • 無法辨識的副檔名會在任何網路流量之前就引發 ValueError
// 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): self
GotenbergConfig::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(): bool
GotenbergBridge::convertFile(string $path): GotenbergConvertResult
GotenbergBridge::convertString(string $bytes, string $originalFilename): GotenbergConvertResult

結果物件會揭露 pdfDatasourceFormat 列舉、isValid()(當內容非空且開頭為 %PDF 時為 true),以及 size()。完整的欄位參考、fromArray() 鍵對映,以及傳輸選擇規則,請見「另請參閱」中連結的 Gotenberg 組態頁面。

描述服務、接好橋接、先探測服務,再轉換一個檔案。

convert-quickstart.php
<?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,而且從不擲出例外;只要 /health 回傳低於 500 的狀態,就表示可用。

正式環境的轉換流程會分別接住每一種失敗型別,只在正確條件下重試,並從呼叫端側限制並行量。下面的 catch 順序是完整窮舉的。

OfficeConverter.php
<?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 用戶端例外),以及等冪的伺服器錯誤(502503504)時重試。一個 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 後處理的範疇,也是商業版能力,並非這個橋接的一部分。