Generate a PDF in a queued job
At a glance
Section titled “At a glance”Heavy PDF generation should not run on the request thread. Each framework
integration gives you a queued-generation API that builds and saves a PDF on a
worker. The HTTP request can return as soon as you dispatch the work. This
guide covers the queued path for Laravel (GeneratePdfJob), Symfony
(GeneratePdfMessage over Messenger), and CodeIgniter 4 (GeneratePdfJob
through codeigniter4/queue).
The prerequisites are:
- NextPDF core and one framework integration are installed.
- A worker transport is configured: a Laravel queue connection, a Symfony
Messenger transport, or a CodeIgniter 4 queue with
codeigniter4/queueinstalled. - A worker process is running for that transport.
This guide assumes your application already has a queue. For queue or Messenger setup, use your framework’s own documentation.
Install
Section titled “Install”Install the integration, then install the queue dependency your framework needs.
composer require nextpdf/laravelcomposer require nextpdf/symfony symfony/messengerCodeIgniter needs the queue package. The integration declares it as a development-only dependency, so require it in the application that runs workers.
composer require nextpdf/codeigniter codeigniter4/queueFor Laravel, configure the queue connection in config/nextpdf.php
(queue.connection, queue.queue, queue.timeout), then run a worker for
that connection.
Conceptual overview
Section titled “Conceptual overview”Each integration uses the same pattern in its own framework style:
- Laravel ships
NextPDF\Laravel\Jobs\GeneratePdfJob, aShouldQueuejob. You dispatch it with an output path and a builder closure. The closure gets a container-resolved document and returns the configured document. On the worker, the job saves the returned document to the path. It also accepts optional success and failure callbacks. - Symfony ships
NextPDF\Symfony\Message\GeneratePdfMessage, areadonlymessage dispatched on the Messenger bus, plusGeneratePdfHandler. The handler resolves a builder by class name from a PSR-11 service locator. You implementNextPDF\Symfony\Message\PdfBuilderInterfacefor each document type. - CodeIgniter 4 ships
NextPDF\CodeIgniter\Jobs\GeneratePdfJob, registered under a name key inConfig\Queue::$jobHandlers. You push the job by its registered name with a builder reference, an output path, and a context array. The builder is a static method limited to theApp\PdfBuildersnamespace.
All three integrations share one security stance: they validate the output path. Symfony and CodeIgniter re-validate it at consume time, because a payload can wait in a queue between dispatch and execution. The builder runs against a fresh document on the worker, so concurrent jobs never share document state.
API surface
Section titled “API surface”| Concern | Laravel | Symfony | CodeIgniter 4 |
|---|---|---|---|
| Queued unit | GeneratePdfJob (ShouldQueue) | GeneratePdfMessage (DTO) + GeneratePdfHandler | GeneratePdfJob (queue handler) |
| Dispatch | GeneratePdfJob::dispatch($path, $builder, $onSuccess, $onFailure) | MessageBusInterface::dispatch(new GeneratePdfMessage(...)) | service('queue')->push($queue, $name, $data) |
| Builder shape | callable(PdfDocumentInterface): PdfDocumentInterface | PdfBuilderInterface::build(Document, array): Document | static fn(Document, array): Document under App\PdfBuilders |
| Path / input guard | Job validates the output path on the worker | DTO validates at construction, handler re-validates at consume | Job confines path to WRITEPATH/pdfs/, allowlists builder namespace |
| Failure surface | failed() after tries; onFailure on terminal failure | Messenger retry strategy; typed validation errors | InvalidArgumentException / QueueException |
Code sample — Quick start
Section titled “Code sample — Quick start”Use this minimal dispatch in each 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),);The output path must end in .pdf; the job validates the path on the worker
before it writes the file.
<?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]); }}In CodeIgniter, push the jobHandlers key ('generate-pdf'), not the job
class string. Register the handler first in 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, ];}Code sample — Production
Section titled “Code sample — Production”A production dispatch wires success and failure callbacks (Laravel), or an explicitly registered builder and a typed handler (Symfony), into a PSR-3 logger. The Laravel example below dispatches with both callbacks.
<?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, ]); }, ); }}The success callback receives the output path. The failure callback receives
the Throwable. The job exhausts tries (default 3) before the failure path
runs. Tune timeout through nextpdf.queue.timeout. The tries and backoff
values are public properties, so subclass GeneratePdfJob to change them.
For Symfony, implement the builder and register it in a service locator. That keeps the handler limited to registered builders.
<?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'For CodeIgniter, implement the builder as a static method under
App\PdfBuilders. The job rejects any builder reference outside that
namespace and any output path outside 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; }}Run the worker for each framework.
php bin/console messenger:consume async --limit=200 --memory-limit=256M --time-limit=3600php spark queue:work pdf-queueRecycle Laravel and Symfony workers with bounded lifetimes
(--limit / --memory-limit / --time-limit) so a leaked allocation in a
dependency cannot grow without bound.
Edge cases & gotchas
Section titled “Edge cases & gotchas”- The builder return value is what gets saved. In every integration, the worker saves the document the builder returns, not the instance it originally resolved. Always return the configured document from the builder.
- Path validation runs on the worker. Symfony validates the output path at
construction and again at consume time. CodeIgniter confines the path to
WRITEPATH/pdfs/and rejects traversal and sibling-prefix paths. A path that was safe at dispatch but unsafe at consume is still rejected. - CodeIgniter pushes the name, not the class. If you push
GeneratePdfJob::classas the job name, the queue rejects it at push time. Push thejobHandlerskey instead. - Laravel callbacks must be passed to the static dispatch. If you build a
job instance and then call
$job->dispatch(...), that call discards the instance and its callbacks. Pass the callbacks toGeneratePdfJob::dispatch(...). - Worker-safe registries. The font registry is a locked process-lifetime singleton, and the image registry is a bounded cache. Documents are fresh per job. Do not request a shared document on the worker.
- Signing in workers. Signed or PDF/A output in a queue job requires a
commercial NextPDF edition installed in the worker environment. Without it,
the signing service resolves to
null. Null-check before signing.
Performance
Section titled “Performance”Moving generation to a queued job removes the full PDF build time from the HTTP
request. The request returns once the work is dispatched. The font and image
registries amortize their setup cost across the worker lifetime, so the
per-job cost is limited to document construction and content emission. Size the
number of in-flight jobs to your worker pool, and pre-populate preload_fonts
(Laravel, Symfony) so font warmup happens once at worker boot rather than on
the first job.
Security notes
Section titled “Security notes”- Queue payloads are attacker-influenced when the broker is reachable, so treat the output path and builder reference in a payload as untrusted. The integrations enforce this with path validation and, in CodeIgniter, a builder namespace allowlist.
- Restrict the worker filesystem permissions to the intended output directory as defense in depth. If a tampered path somehow passes validation, it still cannot escape the directory.
- Log the exception class and a correlation identifier in the failure callback, never the message or trace.
- Never write an empty
catchblock. Every failure callback here logs and carries context.
Each integration’s security-and-operations page covers the full queue threat model: payload validation, callable allowlists, and path confinement.
Conformance
Section titled “Conformance”This guide makes no normative standards claim. Every API call shown is the verified public surface of the named integration. The queued path relies on container-binding guarantees: a fresh document per resolution and the locked font registry. The upstream production-usage pages linked under See also document those guarantees with their PSR citations. This cookbook page restates the usage and defers the citations to those pages.
See also
Section titled “See also”- Return a generated PDF from a controller — the synchronous counterpart.
- Laravel production usage —
GeneratePdfJob, callbacks, and the queue-tuning table. - Symfony production usage — Messenger worker safety and the builder locator.
- CodeIgniter production usage —
GeneratePdfJob,jobHandlers, and path confinement.