在 queue job 中產生 PDF
繁重的 PDF 產生作業不該留在請求執行緒上。每個 framework 整合都提供佇列產生介面,會在 worker(工作行程)上建構並儲存 PDF,因此 HTTP 請求一旦派送工作就能立即回應。本指南涵蓋 Laravel(GeneratePdfJob)、Symfony(透過 Messenger 的 GeneratePdfMessage)與 CodeIgniter 4(GeneratePdfJob,透過 codeigniter4/queue)的佇列路徑。
先決條件如下:
- 已安裝 NextPDF core 與其中一個 framework 整合套件。
- 已設定 worker transport:Laravel 的 queue 連線、Symfony 的 Messenger transport,或 CodeIgniter 4 已安裝
codeigniter4/queue的 queue。 - 該 transport 有一個 worker 行程正在執行。
本指南假設你的應用程式已經有一個 queue。關於 queue 或 Messenger 本身的設定,請參閱你所使用 framework 的官方文件。
先安裝整合套件,再安裝你的 framework 所需的 queue 相依套件。
composer require nextpdf/laravelcomposer require nextpdf/symfony symfony/messengerCodeIgniter 需要 queue 套件。整合套件僅將它宣告為開發階段相依套件,因此你要在實際執行 worker 的應用程式中直接 require 它。
composer require nextpdf/codeigniter codeigniter4/queue在 Laravel 中,於 config/nextpdf.php 中設定 queue 連線(queue.connection、queue.queue、queue.timeout),並為該連線執行一個 worker。
概念總覽
標題為「概念總覽」的區段三個整合各自以自己的方式表達相同概念:
- Laravel 提供
NextPDF\Laravel\Jobs\GeneratePdfJob,這是一個ShouldQueuejob。你以輸出路徑和 builder 閉包來派送它。這個閉包會收到一份由容器 resolve(解析)出來的 document,並回傳設定完成的 document。job 會把回傳的那份 document 儲存到 worker 上的指定路徑。它也接受選用的成功與失敗回呼。 - Symfony 提供
NextPDF\Symfony\Message\GeneratePdfMessage,這是一個派送到 Messenger bus 上的readonly訊息;另外還有GeneratePdfHandler,它會從 PSR-11 service locator 依類別名稱解析出對應的 builder。你要為每一種 document 類型實作NextPDF\Symfony\Message\PdfBuilderInterface。 - CodeIgniter 4 提供
NextPDF\CodeIgniter\Jobs\GeneratePdfJob,並以名稱鍵註冊在Config\Queue::$jobHandlers之下。你以註冊的名稱推送這個 job,並附上 builder 參照、輸出路徑與 context 陣列。builder 是一個限定於App\PdfBuildersnamespace(命名空間)之下的靜態方法。
這三者共用同一套安全立場:輸出路徑都會經過驗證。Symfony 與 CodeIgniter 會在消費時重新驗證一次,因為 payload 可能在派送與執行之間持續留在 queue 中。builder 是針對 worker 上一份全新的 document 執行,所以並行的多個 job 不會共用 document 狀態。
API 介面
標題為「API 介面」的區段| 關注面向 | Laravel | Symfony | CodeIgniter 4 |
|---|---|---|---|
| 佇列單元 | GeneratePdfJob(ShouldQueue) | GeneratePdfMessage(DTO)+ GeneratePdfHandler | GeneratePdfJob(queue handler,佇列處理器) |
| 派送 | GeneratePdfJob::dispatch($path, $builder, $onSuccess, $onFailure) | MessageBusInterface::dispatch(new GeneratePdfMessage(...)) | service('queue')->push($queue, $name, $data) |
| builder 形狀 | callable(PdfDocumentInterface): PdfDocumentInterface | PdfBuilderInterface::build(Document, array): Document | static fn(Document, array): Document,位於 App\PdfBuilders |
| 路徑/輸入防護 | job 會在 worker 上驗證輸出路徑 | DTO 在建構時驗證,handler 在消費時重新驗證 | job 會把路徑限定在 WRITEPATH/pdfs/ 之內,並以允許清單限定 builder namespace |
| 失敗介面 | failed()(在 tries 用盡後呼叫);onFailure 在終端失敗時觸發 | Messenger 重試策略;具型別的驗證錯誤 | InvalidArgumentException / QueueException |
程式碼範例 — 快速上手
標題為「程式碼範例 — 快速上手」的區段以下是在各 framework 中最精簡的派送寫法。
<?php
declare(strict_types=1);
use NextPDF\Contracts\PdfDocumentInterface;use NextPDF\Laravel\Jobs\GeneratePdfJob;
GeneratePdfJob::dispatch( storage_path('app/reports/january-2026.pdf'), static fn (PdfDocumentInterface $document): PdfDocumentInterface => $document ->addPage() ->cell(0, 10, 'January report', newLine: true),);輸出路徑必須以 .pdf 結尾;job 會先在 worker 上驗證路徑,再寫入。
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Pdf\InvoicePdfBuilder;use NextPDF\Symfony\Message\GeneratePdfMessage;use Symfony\Component\HttpFoundation\Response;use Symfony\Component\Messenger\MessageBusInterface;use Symfony\Component\Routing\Attribute\Route;
final class ReportController{ #[Route('/invoice/{id}/queue', name: 'invoice_queue')] public function queue(MessageBusInterface $bus, int $id): Response { $bus->dispatch(new GeneratePdfMessage( builderClass: InvoicePdfBuilder::class, outputPath: '/var/storage/invoices/' . $id . '.pdf', builderContext: ['invoice_id' => $id], ));
return new Response('PDF generation queued.', 202); }}<?php
declare(strict_types=1);
namespace App\Controllers;
use CodeIgniter\HTTP\ResponseInterface;
final class InvoiceController extends BaseController{ public function queueInvoice(int $id): ResponseInterface { service('queue')->push('pdf-queue', 'generate-pdf', [ 'builder' => 'App\\PdfBuilders\\InvoiceBuilder::build', 'outputPath' => WRITEPATH . 'pdfs/invoice-' . $id . '.pdf', 'context' => ['invoice_id' => $id], ]);
return $this->response ->setStatusCode(ResponseInterface::HTTP_ACCEPTED) ->setJSON(['status' => 'queued', 'invoice_id' => $id]); }}在 CodeIgniter 中,你要推送的是 jobHandlers 鍵('generate-pdf'),而不是 job 類別字串。請先在 app/Config/Queue.php 中註冊這個 handler。
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Queue\Config\Queue as BaseQueue;use NextPDF\CodeIgniter\Jobs\GeneratePdfJob;
final class Queue extends BaseQueue{ /** @var array<string, class-string> */ public array $jobHandlers = [ 'generate-pdf' => GeneratePdfJob::class, ];}程式碼範例 — 正式環境
標題為「程式碼範例 — 正式環境」的區段正式環境的派送會接上成功與失敗回呼(Laravel),或明確註冊的 builder 與具型別的 handler(Symfony),並透過 PSR-3 logger 記錄日誌。下方的 Laravel 寫法會在派送時同時帶上兩個回呼。
<?php
declare(strict_types=1);
namespace App\Jobs;
use NextPDF\Contracts\PdfDocumentInterface;use NextPDF\Laravel\Jobs\GeneratePdfJob;use Psr\Log\LoggerInterface;use Throwable;
final class DispatchMonthlyStatement{ public function __construct(private readonly LoggerInterface $logger) {}
public function __invoke(int $accountId): void { // dispatch() is public static: it constructs the job from the // arguments it receives. Pass every argument — including the // callbacks — to the static call, not to a separately built instance. GeneratePdfJob::dispatch( storage_path("app/statements/{$accountId}.pdf"), static fn (PdfDocumentInterface $document): PdfDocumentInterface => $document ->addPage() ->cell(0, 10, "Statement for account {$accountId}", newLine: true), function (string $path) use ($accountId): void { $this->logger->info('Statement PDF written', [ 'account_id' => $accountId, 'path' => $path, ]); }, function (Throwable $exception) use ($accountId): void { $this->logger->error('Statement PDF failed', [ 'account_id' => $accountId, 'exception' => $exception::class, ]); }, ); }}成功回呼會收到輸出路徑;失敗回呼會收到 Throwable。job 會先把 tries(預設 3)用盡,才會進入失敗路徑。要調整 timeout,請透過 nextpdf.queue.timeout。tries 與 backoff 值都是 public 屬性,因此你可以子類別化 GeneratePdfJob 來變更它們。
在 Symfony 中,請實作 builder 並把它註冊到 service locator,這樣 handler 只能存取已註冊的 builder。
<?php
declare(strict_types=1);
namespace App\Pdf;
use NextPDF\Core\Document;use NextPDF\Symfony\Message\PdfBuilderInterface;
final class InvoicePdfBuilder implements PdfBuilderInterface{ /** @param array<string, mixed> $context */ public function build(Document $document, array $context): Document { $document->addPage(); $document->setFont('dejavusans', '', 12); $document->cell(0, 10, 'Invoice #' . $context['invoice_id']);
return $document; }}services: App\Pdf\InvoicePdfBuilder: ~
nextpdf.pdf_builder_locator: class: Symfony\Component\DependencyInjection\ServiceLocator arguments: - 'App\Pdf\InvoicePdfBuilder': '@App\Pdf\InvoicePdfBuilder' tags: ['container.service_locator']
NextPDF\Symfony\Message\GeneratePdfHandler: arguments: $builderLocator: '@nextpdf.pdf_builder_locator'在 CodeIgniter 中,請把 builder 實作成 App\PdfBuilders 之下的一個靜態方法。job 會拒絕該 namespace 以外的任何 builder 參照,也會拒絕 WRITEPATH/pdfs/ 以外的任何輸出路徑。
<?php
declare(strict_types=1);
namespace App\PdfBuilders;
use NextPDF\Core\Document;
final class InvoiceBuilder{ /** @param array<string, mixed> $context */ public static function build(Document $document, array $context): Document { $invoiceId = (int) ($context['invoice_id'] ?? 0);
$document->addPage(); $document->cell(0, 10, "Invoice #{$invoiceId}");
return $document; }}為各 framework 執行 worker。
php bin/console messenger:consume async --limit=200 --memory-limit=256M --time-limit=3600php spark queue:work pdf-queue請用有界的生命週期回收 Laravel 與 Symfony 的 worker(--limit / --memory-limit / --time-limit),這樣某個相依套件外洩的記憶體配置才不會無上限地增長。
邊界情況與陷阱
標題為「邊界情況與陷阱」的區段- builder 回傳的值才是被儲存的內容。 在每個整合中,worker 儲存的都是 builder 回傳的那份 document,而不是一開始解析出來的那個實例。請務必從 builder 回傳設定完成的 document。
- 路徑驗證在 worker 上執行。 Symfony 會在建構時與消費時各驗證一次輸出路徑。CodeIgniter 會把路徑限定在
WRITEPATH/pdfs/之內,並拒絕路徑穿越與同名前綴的路徑。派送時安全、但消費時不安全的路徑仍然會被拒絕。 - CodeIgniter 推送的是名稱,不是類別。 把
GeneratePdfJob::class當成 job 名稱推送,會在推送時被 queue 拒絕。請改推送jobHandlers鍵。 - Laravel 的回呼必須傳給靜態 dispatch。 先建構一個 job 實例、再呼叫
$job->dispatch(...),會丟掉那個實例與它的回呼。請把回呼傳給GeneratePdfJob::dispatch(...)。 - worker 安全的登錄表。 font registry 是一個鎖定的、process 生命週期單例,image registry 則是一個有界的快取。document 在每個 job 都是全新的。請不要在 worker 上取用共用的 document。
- 在 worker 中簽章。 在 queue job 中產生已簽章或 PDF/A 輸出,需要在 worker 環境中安裝商業版 NextPDF;若未安裝,簽章服務會解析為
null。請在簽章前先做 null 檢查。
把產生作業移到 queue job 後,HTTP 請求就不必承擔完整的 PDF 建構時間:工作一旦派送出去,請求就能回應。font 與 image registry 會把設定成本攤平到整個 worker 生命週期,因此每個 job 的成本只剩下 document 建構與內容輸出。請依你的 worker pool 規模調整同時執行的 job 數量,並預先填充 preload_fonts(Laravel、Symfony),讓 font 暖機只在 worker 啟動時做一次,而不是等到第一個 job 才做。
安全注意事項
標題為「安全注意事項」的區段- 當 broker 可被存取時,queue payload 就會受到攻擊者影響,因此請把 payload 中的輸出路徑與 builder 參照都視為不可信任。各整合會以路徑驗證(在 CodeIgniter 中還加上 builder namespace 允許清單)來落實這項防護。
- 請把 worker 檔案系統權限限制在預定的輸出目錄,作為縱深防禦,這樣即使某個被竄改的路徑以某種方式通過了驗證,仍然無法逃出該目錄。
- 請在失敗回呼中記錄例外類別與一個關聯識別碼,絕不要記錄例外訊息本身或堆疊追蹤。
- 絕不要寫出一個空的
catch區塊。這裡的每個失敗回呼都會記錄日誌並帶上 context。
完整的 queue 威脅模型 — payload 驗證、callable 允許清單與路徑限定 — 記載在各整合的安全與維運頁面中。
符合性
標題為「符合性」的區段本指南不提出任何規範性的標準主張。文中展示的每個 API 呼叫,都是所指名整合經過驗證的公開介面。佇列路徑所仰賴的容器繫結保證(每次解析都得到一份全新的 document、鎖定的 font registry),連同其 PSR 引用,都記載在「另請參閱」下方連結的上游正式環境用法頁面中。本 cookbook 頁面只重述用法,引用則保留在那些頁面中。
另請參閱
標題為「另請參閱」的區段- 從控制器回傳產生的 PDF — 同步版的對應做法。
- Laravel 正式環境用法 —
GeneratePdfJob、回呼,以及 queue 調校表。 - Symfony 正式環境用法 — Messenger 的 worker 安全性與 builder locator。
- CodeIgniter 正式環境用法 —
GeneratePdfJob、jobHandlers,以及路徑限定。