キュー投入済みジョブで PDF を生成する
負荷の大きい PDF 生成は、リクエストスレッドで実行すべきではありません。各フレームワーク統合は、ワーカー上で PDF を構築して保存する、キュー生成用のサーフェスを提供します。そのため、作業がディスパッチされると、HTTP リクエストにはすぐに応答できます。このガイドでは、Laravel(GeneratePdfJob)、Symfony(Messenger を介した GeneratePdfMessage)、CodeIgniter 4(GeneratePdfJob を codeigniter4/queue 経由で実行)について、キュー経由のパスを扱います。
前提条件は次のとおりです。
- NextPDF コアと、いずれか 1 つのフレームワーク統合がインストールされていること。
- ワーカートランスポートが構成されていること: Laravel のキュー接続、Symfony Messenger トランスポート、または
codeigniter4/queueをインストールした CodeIgniter 4 のキュー。 - そのトランスポート用のワーカープロセスが稼働していること。
このガイドは、すでにキューを備えているアプリケーションを前提としています。キューまたは Messenger のセットアップそのものについては、各フレームワークのドキュメントを参照してください。
インストール
「インストール」という見出しのセクション統合をインストールし、続けてフレームワークが必要とするキュー依存関係をインストールします。
composer require nextpdf/laravelcomposer require nextpdf/symfony symfony/messengerCodeIgniter にはキューパッケージが必要です。統合はこれを開発専用の依存関係として宣言しているため、ワーカーを実行するアプリケーションで直接 require してください。
composer require nextpdf/codeigniter codeigniter4/queueLaravel の場合は、config/nextpdf.php(queue.connection、queue.queue、queue.timeout)でキュー接続を構成し、その接続用のワーカーを実行します。
概念の概観
「概念の概観」という見出しのセクション各統合は、同じ設計思想をそれぞれ独自の方法で表現します。
- Laravel は
NextPDF\Laravel\Jobs\GeneratePdfJob(ShouldQueueジョブ)を提供します。出力パスとビルダークロージャを指定してディスパッチします。クロージャはコンテナによって resolve(解決)されたドキュメントを受け取り、構成済みのドキュメントを返します。ジョブは、返されたドキュメントをワーカー上でパスに保存します。省略可能な成功コールバックと失敗コールバックも受け取ります。 - Symfony は、Messenger バス上でディスパッチされる
NextPDF\Symfony\Message\GeneratePdfMessage(readonlyメッセージ)と、PSR-11 サービスロケーターからクラス名でビルダーを解決するGeneratePdfHandlerを提供します。ドキュメントの種類ごとにNextPDF\Symfony\Message\PdfBuilderInterfaceを実装します。 - CodeIgniter 4 は
NextPDF\CodeIgniter\Jobs\GeneratePdfJobを提供し、Config\Queue::$jobHandlersの名前キーで登録されます。ビルダー参照、出力パス、コンテキスト配列とともに、登録された名前でジョブをプッシュします。ビルダーはApp\PdfBuilders名前空間に限定された静的メソッドです。
3 つはいずれも、同じセキュリティ方針を共有しています。つまり、出力パスは検証されます。ペイロードはディスパッチから実行までの間キュー内に滞留する可能性があるため、Symfony と CodeIgniter は消費時にも再検証します。ビルダーはワーカー上の新しいドキュメントに対して実行されるため、並行するジョブがドキュメントの状態を共有することはありません。
API サーフェス
「API サーフェス」という見出しのセクション| 関心事 | Laravel | Symfony | CodeIgniter 4 |
|---|---|---|---|
| キューに入れる単位 | GeneratePdfJob(ShouldQueue) | GeneratePdfMessage(DTO)+ GeneratePdfHandler | GeneratePdfJob(キューハンドラー) |
| ディスパッチ | GeneratePdfJob::dispatch($path, $builder, $onSuccess, $onFailure) | MessageBusInterface::dispatch(new GeneratePdfMessage(...)) | service('queue')->push($queue, $name, $data) |
| ビルダーの形式 | callable(PdfDocumentInterface): PdfDocumentInterface | PdfBuilderInterface::build(Document, array): Document | static fn(Document, array): Document。名前空間は App\PdfBuilders 配下 |
| パス/入力ガード | ジョブがワーカー上で出力パスを検証する | DTO が構築時に検証し、ハンドラーが消費時に再検証する | ジョブがパスを WRITEPATH/pdfs/ に限定し、ビルダー名前空間を許可リストで管理する |
| 失敗時の扱い | failed()(tries 後)。onFailure は最終的な失敗時 | Messenger の再試行戦略。型付けされた検証エラー | InvalidArgumentException / QueueException |
コードサンプル — クイックスタート
「コードサンプル — クイックスタート」という見出しのセクション各フレームワークでの最小構成のディスパッチ例です。
<?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 で終わる必要があります。ジョブは書き込み前に、ワーカー上でパスを検証します。
<?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')をプッシュします。まず app/Config/Queue.php でハンドラーを登録します。
<?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)、または明示的に登録されたビルダーと型付けされたハンドラー(Symfony)を結び付け、PSR-3 ロガー経由でログを記録します。以下の 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 を受け取ります。ジョブは tries(デフォルトは 3)を使い切った後に、失敗パスを実行します。timeout は nextpdf.queue.timeout で調整します。tries と backoff の値は public プロパティのため、変更するには GeneratePdfJob をサブクラス化します。
Symfony の場合は、ビルダーを実装してサービスロケーターに登録し、ハンドラーから到達できるビルダーを登録済みのものだけに制限します。
<?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 の場合は、ビルダーを App\PdfBuilders 配下の静的メソッドとして実装します。ジョブは、その名前空間外のビルダー参照と、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; }}各フレームワークでワーカーを実行します。
php bin/console messenger:consume async --limit=200 --memory-limit=256M --time-limit=3600php spark queue:work pdf-queue依存関係によるメモリ割り当てのリークが際限なく増大しないように、Laravel と Symfony のワーカーは上限付きのライフタイム(--limit / --memory-limit / --time-limit)でリサイクルします。
エッジケースと注意点
「エッジケースと注意点」という見出しのセクション- 保存されるのはビルダーの戻り値です。 どの統合でも、ワーカーは最初に解決されたインスタンスではなく、ビルダーが返したドキュメントを保存します。ビルダーからは常に、構成済みのドキュメントを返してください。
- パスの検証はワーカー上で実行されます。 Symfony は出力パスを構築時に検証し、消費時にも再度検証します。CodeIgniter はパスを
WRITEPATH/pdfs/に限定し、トラバーサルおよび同階層のプレフィックスを持つパスを拒否します。ディスパッチ時には安全でも消費時には安全でなくなったパスは、依然として拒否されます。 - CodeIgniter はクラスではなく名前をプッシュします。 ジョブ名として
GeneratePdfJob::classをプッシュすると、プッシュ時にキューによって拒否されます。代わりにjobHandlersキーをプッシュしてください。 - Laravel のコールバックは静的な dispatch に渡す必要があります。 ジョブインスタンスを構築してから
$job->dispatch(...)を呼び出すと、そのインスタンスとコールバックは破棄されます。コールバックはGeneratePdfJob::dispatch(...)に渡してください。 - ワーカーで安全なレジストリ。 フォントレジストリは、プロセスのライフタイム中はロックされたシングルトンであり、画像レジストリは上限付きのキャッシュです。ドキュメントはジョブごとに新しく生成されます。ワーカー上で共有ドキュメントを要求しないでください。
- ワーカーでの署名。 キュージョブで署名済み出力または PDF/A 出力を行うには、ワーカー環境に商用版の NextPDF エディションがインストールされている必要があります。それがない場合、署名サービスは
nullに解決されます。署名の前に null チェックを行ってください。
パフォーマンス
「パフォーマンス」という見出しのセクション生成をキュージョブに移すと、PDF の構築時間全体を HTTP リクエストの処理時間から外せます。つまり、作業がディスパッチされた時点でリクエストが返ります。フォントレジストリと画像レジストリはセットアップコストをワーカーのライフタイム全体にわたって分散させるため、ジョブごとのコストはドキュメントの構築とコンテンツの出力に限られます。処理中のジョブ数をワーカープールに合わせて調整し、preload_fonts(Laravel、Symfony)をあらかじめ設定して、フォントのウォームアップが最初のジョブではなくワーカー起動時に一度だけ行われるようにします。
セキュリティに関する注意
「セキュリティに関する注意」という見出しのセクション- ブローカーに到達できる場合、キューのペイロードは攻撃者の影響を受ける可能性があるため、ペイロード内の出力パスとビルダー参照は信頼できないものとして扱ってください。各統合は、パス検証と、CodeIgniter ではビルダー名前空間の許可リストによってこの扱いを強制します。
- 多層防御として、ワーカーのファイルシステム権限を意図した出力ディレクトリに制限し、何らかの理由で検証を通過した改ざん済みのパスでも、そのディレクトリの外へ出られないようにします。
- 失敗コールバックでは、例外クラスと相関識別子をログに記録し、メッセージやトレースは決して記録しないでください。
- 空の
catchブロックは決して記述しないでください。ここに示す失敗コールバックはいずれも、コンテキスト付きでログを記録します。
キューの完全な脅威モデル(ペイロード検証、callable の許可リスト、パスの限定)は、各統合のセキュリティと運用のページに記載されています。
このガイドは、規範的な標準に関する主張を一切行いません。ここで示すすべての API 呼び出しは、当該統合の検証済みの公開サーフェスです。キュー経由のパスが依拠するコンテナバインディングの保証(解決ごとに新しいドキュメント、ロックされたフォントレジストリ)は、「関連項目」でリンクしている上流の本番利用ページに、その PSR の引用とともに記載されています。このクックブックページでは使い方を改めて述べ、引用についてはそれらのページに委ねます。
- コントローラーから生成済みの PDF を返す — 同期版の対応物。
- Laravel の本番利用 —
GeneratePdfJob、コールバック、キューチューニングの表。 - Symfony の本番利用 — Messenger のワーカー安全性とビルダーロケーター。
- CodeIgniter の本番利用 —
GeneratePdfJob、jobHandlers、パスの限定。