跳到內容

NextPDF Symfony 正式環境部署

此套件設計用於長時間執行的 PHP runtime。 Document 不共用、字型登錄表(font registry)在暖機後鎖定,影像快取則會在每次請求之間重置。 大型 PDF 請採用串流,並將繁重工作卸載到 Messenger worker。

長時間執行的 runtime 會讓 container(容器)跨請求保持存活,因此每次請求的狀態都不可外洩。 FrankenPHP、RoadRunner 與 Messenger worker 的運作方式皆是如此。 此套件的 services.php 定義了以下生命週期,並已依服務定義完成驗證:

  • Document — 不共用。 nextpdf.document(以及 PdfDocumentInterface / Document 別名)每次都會 resolve(解析)為全新的實例。 依 PSR-11,container 每次針對同一個 id 呼叫 get() 時,都可以合法回傳不同的值(PSR-11 §1.1.2)。 每次請求都會解析出各自的 document。 絕不要跨請求持有同一份 document。
  • FontRegistry — 共用且鎖定。 此登錄表是整個 process 生命週期內的單例(singleton)。 在 warmup() 之後(當 preload_fonts 非空時),編譯器階段會呼叫 lock()。 這道鎖可避免執行期變動,進而避免跨請求的字型狀態污染。
  • ImageRegistry — 共用,每次請求重置。 這個有上限的最近最少使用(LRU)影像快取是共用的,但標記為 kernel.reset 並帶有 reset 方法,因此在遵守 kernel.reset 的 runtime 下,Symfony 會在請求之間清除它。
  • EInvoice 合約 — 不共用。 當存在 Premium 實作時,embedder、validator、profile 與 schematron 服務都會註冊為不共用。 每次呼叫所用的 parser(剖析器)context 絕不會跨請求外洩。

請注入 PdfFactory(共用、無狀態的組態持有者),並在每次請求中呼叫 create()

public function __construct(private readonly PdfFactory $pdf) {}
public function action(): Response
{
$doc = $this->pdf->create(); // fresh, disposable
// ... build ...
return PdfResponse::inline($doc, 'document.pdf');
}

不要將 Documentnextpdf.document 注入本身為共用、且會跨請求持有狀態的服務。 請改在請求範圍內的方法中解析它。

PdfResponse::streamDownload()streamInline() 會回傳一個 StreamedResponse。 它的 callback 會以 64 KB 為單位輸出 PDF 主體,並在每個區塊後 flush。 這會將大型 document 的回應緩衝區維持在可控範圍內。 以下兩種取捨都已對照 PdfResponse 驗證:

  • 串流版本刻意 省略 Content-Length(回應物件事先並不知道主體大小)。 下載進度列與某些 proxy 偏好可預先得知的長度。 當 document 小到足以保存在記憶體中、且需要 content length 時,請改用非串流的 download()inline()
  • 串流版本會送出與緩衝版本相同的安全標頭,以及相同的 Cache-Control: private, max-age=0, must-revalidate

對於數 MB 的報表與批次匯出,請選擇串流版本。 對於小型、對延遲敏感的回應,則選擇緩衝版本。

當請求必須快速回應,或繪製過程耗用大量 CPU 時,請將產生工作卸載到 Messenger。

  1. 為每一種 document 類型實作 PdfBuilderInterface
  2. container.service_locator 中註冊 builder,並將它連接到 GeneratePdfHandler$builderLocator
  3. GeneratePdfMessage 路由到耐久的 transport。
  4. 以有界的生命週期執行 worker。

回收 worker,避免第三方相依套件外洩的已配置記憶體無限制成長:

Terminal window
php bin/console messenger:consume async \
--limit=200 \
--memory-limit=256M \
--time-limit=3600

此套件的 messenger.timeoutmessenger.retries 組態鍵記錄了預期的每則訊息逾時與重試額度。 請透過 Symfony 的重試策略與 worker 旗標落實對應行為。

GeneratePdfMessage 在建構時驗證輸出路徑。 接著 GeneratePdfHandler 會在執行期間、寫入磁碟之前再次驗證它。 這種兩階段檢查對非同步工作很重要。 訊息可能在派發與消費之間停留在佇列裡,因此 handler 不會盲目信任佇列中的路徑。 請將 worker 的檔案系統權限限制在預定的輸出目錄,作為縱深防禦。

其中的 FontRegistryImageRegistry 服務接受一個選用的 Psr\Log\LoggerInterface(以 nullOnInvalid() 繫結)。 當應用程式提供 logger 時,這些登錄表可以透過它輸出診斷資訊。 在 PSR-3 logger 合約(PSR-3)之下,logger 是選用、可替換的協作者。 若要取得請求層級的可見度,請在你的應用程式碼中,於 PdfFactory::create() 與 Messenger handler 周圍記錄日誌。 事故分流時請使用 messenger:consume -vv

  • 在應用程式的 composer.json 中只釘選單一個 nextpdf/core 主版本 (此套件接受 ^3.0 || ^5.2)。
  • 確保部署的 PHP 映像中已啟用 ext-mbstringext-zlib(否則此套件會在啟動時快速失敗)。
  • 為你的 document 所用的字型預先填入 preload_fonts,讓登錄表在啟動時暖機並鎖定,而非在首次請求時才進行。
  • 若你仰賴跨部署保留的快取產物,請將 cache_path 指向可寫入、可持久化的位置。 否則使用 %kernel.cache_dir% 預設值即可。
  • 請在部署期間執行 php bin/console cache:warmup,讓編譯後的 container 本體(含選用擴充功能的探測)在流量到來前先建置完成。
  • 正式環境的非同步工作請使用耐久的 Messenger transport(而非 sync),並以 --limit / --memory-limit / --time-limit 回收 worker。
  • 串流回應位於緩衝型 proxy 之後 —— 若 proxy 會緩衝整個主體,就會抵銷記憶體上的好處。 請設定 proxy 以串流方式處理 PDF 回應,或在該處改用緩衝回應。
  • kernel.reset 未被遵守 —— 在不會呼叫 kernel.reset 的 runtime 下,影像快取會受 image_cache_mb 限制,但不會在請求之間清除;請據此設定上限大小。
  • 跨請求持有 document —— 從先前請求擷取並保留的 Document 會攜帶過時狀態。 請一律透過 PdfFactory 在每次請求時解析。

每一列都是本頁提出的一項規範性主張,並釘選到一個來自受控 SDO 語料庫的完整 64 位十六進位 reference_id。 來源(provenance,即語料庫 manifest 與檢索 transport)記錄於 _sidecars/rag-citations.yaml

規格條款參考 ID(reference_id)主張
PSR-11psr_11_container#1.1.2.p3.b不共用服務:每次解析得到不同的值
PSR-3psr_3_logger#x3.p17選用的 logger 協作者
  • /integrations/symfony/configuration/ —— 服務生命週期與參數。
  • /integrations/symfony/security-and-operations/ —— 回應標頭、路徑驗證、金鑰處理。
  • /integrations/symfony/troubleshooting/ —— 啟動與執行期診斷。
  • /integrations/symfony/quickstart/ —— 最小化的非同步設定。