コンテンツにスキップ

キュー投入済みジョブで PDF を生成する

負荷の大きい PDF 生成は、リクエストスレッドで実行すべきではありません。各フレームワーク統合は、ワーカー上で PDF を構築して保存する、キュー生成用のサーフェスを提供します。そのため、作業がディスパッチされると、HTTP リクエストにはすぐに応答できます。このガイドでは、Laravel(GeneratePdfJob)、Symfony(Messenger を介した GeneratePdfMessage)、CodeIgniter 4(GeneratePdfJobcodeigniter4/queue 経由で実行)について、キュー経由のパスを扱います。

前提条件は次のとおりです。

  • NextPDF コアと、いずれか 1 つのフレームワーク統合がインストールされていること。
  • ワーカートランスポートが構成されていること: Laravel のキュー接続、Symfony Messenger トランスポート、または codeigniter4/queue をインストールした CodeIgniter 4 のキュー。
  • そのトランスポート用のワーカープロセスが稼働していること。

このガイドは、すでにキューを備えているアプリケーションを前提としています。キューまたは Messenger のセットアップそのものについては、各フレームワークのドキュメントを参照してください。

統合をインストールし、続けてフレームワークが必要とするキュー依存関係をインストールします。

Terminal window
composer require nextpdf/laravel
Terminal window
composer require nextpdf/symfony symfony/messenger

CodeIgniter にはキューパッケージが必要です。統合はこれを開発専用の依存関係として宣言しているため、ワーカーを実行するアプリケーションで直接 require してください。

Terminal window
composer require nextpdf/codeigniter codeigniter4/queue

Laravel の場合は、config/nextpdf.phpqueue.connectionqueue.queuequeue.timeout)でキュー接続を構成し、その接続用のワーカーを実行します。

各統合は、同じ設計思想をそれぞれ独自の方法で表現します。

  • LaravelNextPDF\Laravel\Jobs\GeneratePdfJobShouldQueue ジョブ)を提供します。出力パスとビルダークロージャを指定してディスパッチします。クロージャはコンテナによって resolve(解決)されたドキュメントを受け取り、構成済みのドキュメントを返します。ジョブは、返されたドキュメントをワーカー上でパスに保存します。省略可能な成功コールバックと失敗コールバックも受け取ります。
  • Symfony は、Messenger バス上でディスパッチされる NextPDF\Symfony\Message\GeneratePdfMessagereadonly メッセージ)と、PSR-11 サービスロケーターからクラス名でビルダーを解決する GeneratePdfHandler を提供します。ドキュメントの種類ごとに NextPDF\Symfony\Message\PdfBuilderInterface を実装します。
  • CodeIgniter 4NextPDF\CodeIgniter\Jobs\GeneratePdfJob を提供し、Config\Queue::$jobHandlers の名前キーで登録されます。ビルダー参照、出力パス、コンテキスト配列とともに、登録された名前でジョブをプッシュします。ビルダーは App\PdfBuilders 名前空間に限定された静的メソッドです。

3 つはいずれも、同じセキュリティ方針を共有しています。つまり、出力パスは検証されます。ペイロードはディスパッチから実行までの間キュー内に滞留する可能性があるため、Symfony と CodeIgniter は消費時にも再検証します。ビルダーはワーカー上の新しいドキュメントに対して実行されるため、並行するジョブがドキュメントの状態を共有することはありません。

関心事LaravelSymfonyCodeIgniter 4
キューに入れる単位GeneratePdfJobShouldQueueGeneratePdfMessage(DTO)+ GeneratePdfHandlerGeneratePdfJob(キューハンドラー)
ディスパッチGeneratePdfJob::dispatch($path, $builder, $onSuccess, $onFailure)MessageBusInterface::dispatch(new GeneratePdfMessage(...))service('queue')->push($queue, $name, $data)
ビルダーの形式callable(PdfDocumentInterface): PdfDocumentInterfacePdfBuilderInterface::build(Document, array): Documentstatic fn(Document, array): Document。名前空間は App\PdfBuilders 配下
パス/入力ガードジョブがワーカー上で出力パスを検証するDTO が構築時に検証し、ハンドラーが消費時に再検証するジョブがパスを WRITEPATH/pdfs/ に限定し、ビルダー名前空間を許可リストで管理する
失敗時の扱いfailed()tries 後)。onFailure は最終的な失敗時Messenger の再試行戦略。型付けされた検証エラーInvalidArgumentException / QueueException

各フレームワークでの最小構成のディスパッチ例です。

Laravel: dispatch GeneratePdfJob
<?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 で終わる必要があります。ジョブは書き込み前に、ワーカー上でパスを検証します。

Symfony: dispatch GeneratePdfMessage from a controller
<?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);
}
}
CodeIgniter 4: push GeneratePdfJob by its registered name
<?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 でハンドラーを登録します。

CodeIgniter 4: 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 の例では、両方のコールバックを指定してディスパッチします。

Laravel: app/Jobs/DispatchMonthlyStatement.php
<?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)を使い切った後に、失敗パスを実行します。timeoutnextpdf.queue.timeout で調整します。triesbackoff の値は public プロパティのため、変更するには GeneratePdfJob をサブクラス化します。

Symfony の場合は、ビルダーを実装してサービスロケーターに登録し、ハンドラーから到達できるビルダーを登録済みのものだけに制限します。

Symfony: src/Pdf/InvoicePdfBuilder.php
<?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;
}
}
Symfony: config/services.yaml (builder locator)
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/ の外にある出力パスをすべて拒否します。

CodeIgniter 4: app/PdfBuilders/InvoiceBuilder.php
<?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;
}
}

各フレームワークでワーカーを実行します。

Terminal window
php bin/console messenger:consume async --limit=200 --memory-limit=256M --time-limit=3600
Terminal window
php 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 の引用とともに記載されています。このクックブックページでは使い方を改めて述べ、引用についてはそれらのページに委ねます。