在队列任务中生成 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,或已安装
codeigniter4/queue的 CodeIgniter 4 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 则是一个有界缓存。每个 job 中的 document 都是全新的。请不要在 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,以及路径限定。