Symfony 開發者指南
Symfony 套件採 service 優先設計。注入 PdfFactory,對每份文件呼叫 create(),並用 Messenger builder 處理非同步產生。由於每次呼叫都會回傳一份全新文件,這個 factory 可以註冊為 container service。
當你設計 controller、service、Messenger handler,或圍繞 nextpdf/symfony 的 bundle 層擴充點時,請參考本指南。
架構邊界
標題為「架構邊界」的區段| 層級 | 負責方 | 職責 | 不要放這裡 |
|---|---|---|---|
| Controller(控制器) | 應用程式 | 對請求進行授權、收集輸入,並回傳 PdfResponse。 | 跨多種使用情境共用的 PDF 版面。 |
| 應用程式 service | 應用程式 | 載入領域資料,並選定一個 builder。 | Symfony container 編譯邏輯。 |
| Builder service(建構器服務) | 應用程式 | 實作 PdfBuilderInterface,供同步或排入佇列的文件建構使用。 | 請求物件、entity manager,或無法序列化的 context。 |
| Symfony bundle(套件) | nextpdf/symfony | 註冊 service、config tree、選用的 extension pass、回應輔助工具,以及 Messenger DTO。 | 租戶專屬的儲存策略。 |
| 核心引擎 | nextpdf/nextpdf | 撰寫並序列化文件。 | Symfony 回應或 Messenger 行為。 |
執行期生命週期
標題為「執行期生命週期」的區段| 階段 | 行為 | 開發者動作 |
|---|---|---|
| Bundle 啟動 | NextPdfBundle::build() 會註冊選用擴充功能的偵測。 | 讓 Symfony 自動探索這個 bundle,或在 bundles.php 中手動註冊。 |
| 載入設定 | NextPdfExtension::load() 會處理 nextpdf: 設定並載入 service 定義。 | 讓設定維持明確,並能感知環境。 |
| 使用 factory | PdfFactory::create() 會回傳一份全新且已設定好的文件。 | 不要把文件存放在 service 裡。 |
| Controller 輸出 | PdfResponse 會把完成的文件轉成回應。 | 使用這個輔助工具,不要手動組裝標頭。 |
| Messenger 派發 | GeneratePdfMessage 會攜帶 builder 類別、輸出路徑,以及可序列化的 context。 | 讓 context 保持精簡,並以純量為主。 |
| 處理訊息 | GeneratePdfHandler 會透過 service locator resolve(解析)並取得 builder,再儲存文件。 | 讓 builder 維持決定性與冪等性。 |
建議的應用程式結構
標題為「建議的應用程式結構」的區段| 路徑 | 用途 |
|---|---|
src/Pdf/Builder/* | 實作 PdfBuilderInterface 的 service。 |
src/Pdf/Data/* | 作為 builder context 使用的小型 DTO 或陣列。 |
src/Pdf/Storage/* | 儲存根目錄的選擇與輸出檔名策略。 |
src/Controller/* | 同步回應的進入點。 |
tests/Pdf/* | builder、回應、Messenger 與設定的測試。 |
優先採用 builder service,而非靜態輔助函式。這些 service 易於標記、裝飾、測試,也方便從 Messenger 使用。
<?php
namespace App\Pdf\Builder;
use NextPDF\Core\Document;use NextPDF\Symfony\Message\PdfBuilderInterface;
final readonly class InvoicePdfBuilder implements PdfBuilderInterface{ public function build(Document $document, array $context): Document { $document->setTitle((string) $context['title']) ->addPage() ->writeHtml((string) $context['html']);
return $document; }}同步回應模式
標題為「同步回應模式」的區段<?php
namespace App\Controller;
use App\Pdf\Builder\InvoicePdfBuilder;use NextPDF\Symfony\Http\PdfResponse;use NextPDF\Symfony\Service\PdfFactory;
final readonly class InvoiceController{ public function __invoke( PdfFactory $factory, InvoicePdfBuilder $builder, ) { $document = $builder->build($factory->create(), [ 'title' => 'Invoice 1234', 'html' => '<h1>Invoice 1234</h1>', ]);
return PdfResponse::download($document, 'invoice-1234.pdf'); }}讓 controller 的 context 保持精簡。如果某個 builder 需要許多領域物件,就把這段協調邏輯移到應用程式 service,再把 DTO 或正規化後的陣列傳給 builder。
Messenger 模式
標題為「Messenger 模式」的區段GeneratePdfMessage 會在派發前驗證 builder 類別與輸出路徑。 handler 會在執行時再次驗證該路徑。
<?php
use App\Pdf\Builder\InvoicePdfBuilder;use NextPDF\Symfony\Message\GeneratePdfMessage;
$bus->dispatch(new GeneratePdfMessage( builderClass: InvoicePdfBuilder::class, outputPath: $projectDir . '/var/pdfs/invoice-1234.pdf', builderContext: [ 'title' => 'Invoice 1234', 'html' => '<h1>Invoice 1234</h1>', ],));不要把 Doctrine entity、開啟中的串流、closure、請求物件或 service 物件放進 builderContext。
擴充點
標題為「擴充點」的區段| 擴充點 | 適用情境 | 限制 |
|---|---|---|
PdfFactory service 裝飾 | 在文件交給 controller 之前,套用應用程式層級的預設值。 | 必須保留全新文件的語意。 |
PdfBuilderInterface | 定義可排入佇列或可重複使用的文件 builder。 | 必須回傳一個 Document。 |
OptionalExtensionPass | 在編譯期啟用選用的 Artisan 或 Premium 功能。 | 可用與否取決於 container 編譯狀態,而非請求狀態。 |
| Symfony config tree(組態樹) | 預設值、PDF/A、renderer 設定、簽章、TSA、Messenger。 | 無效的設定應在 container 建置時就失敗。 |
GeneratePdfHandler service 接線 | 限制排入佇列的訊息可以存取哪些 builder。 | service locator 應只暴露經核准的 builder service。 |
開發流程
標題為「開發流程」的區段- 新增一個對輸入具決定性的 builder service。
- 在 controller 或 service 中使用
PdfFactory::create()。 - 針對檔名、內容類型與標頭新增回應測試。
- 當同一份文件必須以非同步方式產生時,為 Messenger 註冊該 builder。
- 針對類別名稱、輸出路徑與 context 結構,新增無效訊息的測試。
- 以最小設定與正式環境設定,新增一個 container 編譯測試。
- 在與正式環境相同的 PHP 設定下,量測 render 時間與記憶體用量。
失敗處理
標題為「失敗處理」的區段| 失敗 | 應在何處處理 | 建議的回應 |
|---|---|---|
| 無效的設定 | Container 編譯。 | 在流量抵達應用程式之前讓部署失敗。 |
| 缺少 builder service | Messenger handler 測試與 service tag。 | 讓訊息失敗,並通知負責的團隊。 |
| 不安全的輸出路徑 | 訊息建構子與儲存策略。 | 在派發前就拒絕;handler 的驗證則保留作為縱深防禦。 |
| 選用擴充功能無法使用 | compiler pass 與 factory 行為。 | 停用選用功能,或明確要求安裝。 |
| service 轉換或 render 失敗 | builder 邊界。 | 除非該使用情境有書面記載的後援機制,否則一律 fail closed。 |
安全的預設值
標題為「安全的預設值」的區段| 關注點 | 預設值 | 何時覆寫 |
|---|---|---|
| Factory 生命週期 | Container service(容器服務)。 | 保留這項設定;factory 之所以安全,是因為它只負責建立文件。 |
| 文件生命週期 | 一個工作單元。 | 絕不要跨請求或跨訊息共用。 |
| 輸出路徑驗證 | 訊息建構子與 handler。 | 在應用程式碼中加入租戶或儲存根目錄的限制。 |
| 回應檔名 | document.pdf。 | 以淨化過的業務識別碼覆寫。 |
| Messenger transport(Messenger 傳輸) | async。 | 當 PDF 工作負載很重時,使用專屬的 transport。 |
測試檢查清單
標題為「測試檢查清單」的區段- container 測試會以最小設定與正式環境設定編譯這個 bundle。
- 回應測試會驗證安全標頭與檔名處理。
- Messenger 測試會驗證無效路徑與無效的 builder 類別名稱在派發前就失敗。
- handler 測試會使用真實的 builder service 與暫存輸出目錄。
- builder 測試會 render 一份具代表性的文件,並在類似正式環境的檔案系統權限下儲存它。
- 選用擴充功能的測試涵蓋 Artisan 不可用、Premium 不可用,以及已設定的 PDF/A profile 行為。