Ga naar inhoud

PDF genereren in een queued job

Zware PDF-generatie hoort niet op de request-thread te draaien. Elke framework-integratie biedt een API voor queued generatie waarmee een worker een PDF bouwt en opslaat. De HTTP-request kan terugkeren zodra je het werk dispatcht. Deze handleiding behandelt het queued-pad voor Laravel (GeneratePdfJob), Symfony (GeneratePdfMessage via Messenger) en CodeIgniter 4 (GeneratePdfJob via codeigniter4/queue).

De vereisten zijn:

  • NextPDF-core en één framework-integratie zijn geïnstalleerd.
  • Er is een worker-transport geconfigureerd: een Laravel-queueverbinding, een Symfony-Messenger-transport of een CodeIgniter 4-queue met codeigniter4/queue geïnstalleerd.
  • Er draait een worker-proces voor dat transport.

Deze handleiding gaat ervan uit dat je applicatie al een queue heeft. Gebruik voor het opzetten van de queue of Messenger de documentatie van je eigen framework.

Installeer de integratie en daarna de queue-afhankelijkheid die je framework nodig heeft.

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

CodeIgniter heeft het queue-pakket nodig. De integratie declareert dit als een development-only-afhankelijkheid, dus voeg het toe aan de applicatie die workers draait.

Terminal window
composer require nextpdf/codeigniter codeigniter4/queue

Configureer in Laravel de queueverbinding in config/nextpdf.php (queue.connection, queue.queue, queue.timeout) en draai daarna een worker voor die verbinding.

Elke integratie gebruikt hetzelfde patroon, telkens in de stijl van het framework:

  • Laravel levert NextPDF\Laravel\Jobs\GeneratePdfJob, een ShouldQueue-job. Je dispatcht die met een uitvoerpad en een builder-closure. De closure krijgt een door de container opgelost document en retourneert het geconfigureerde document. Op de worker slaat de job het geretourneerde document op naar het pad. Hij accepteert ook optionele success- en failure-callbacks.
  • Symfony levert NextPDF\Symfony\Message\GeneratePdfMessage, een readonly-message die op de Messenger-bus wordt gedispatcht, plus GeneratePdfHandler. De handler lost een builder op basis van de klassenaam op uit een PSR-11-servicelocator. Je implementeert NextPDF\Symfony\Message\PdfBuilderInterface voor elk documenttype.
  • CodeIgniter 4 levert NextPDF\CodeIgniter\Jobs\GeneratePdfJob, geregistreerd onder een naamsleutel in Config\Queue::$jobHandlers. Je pusht de job met de geregistreerde naam, samen met een builder-referentie, een uitvoerpad en een context-array. De builder is een statische methode die beperkt is tot de App\PdfBuilders-namespace.

Alle drie de integraties hanteren dezelfde beveiligingshouding: ze valideren het uitvoerpad. Symfony en CodeIgniter hervalideren het op het moment van consumptie, omdat een payload tussen dispatch en uitvoering in een queue kan wachten. De builder draait op de worker tegen een vers document, zodat gelijktijdige jobs nooit document-state delen.

AspectLaravelSymfonyCodeIgniter 4
Queued eenheidGeneratePdfJob (ShouldQueue)GeneratePdfMessage (DTO) + GeneratePdfHandlerGeneratePdfJob (queue-handler)
DispatchGeneratePdfJob::dispatch($path, $builder, $onSuccess, $onFailure)MessageBusInterface::dispatch(new GeneratePdfMessage(...))service('queue')->push($queue, $name, $data)
Builder-vormcallable(PdfDocumentInterface): PdfDocumentInterfacePdfBuilderInterface::build(Document, array): Documentstatic fn(Document, array): Document onder App\PdfBuilders
Pad-/invoerbeschermingJob valideert het uitvoerpad op de workerDTO valideert bij constructie, handler hervalideert bij consumptieJob beperkt het pad tot WRITEPATH/pdfs/, plaatst de builder-namespace op een allowlist
Foutoppervlakfailed() na tries; onFailure bij definitieve mislukkingMessenger-retrystrategie; getypeerde validatiefoutenInvalidArgumentException / QueueException

Gebruik deze minimale dispatch in elk framework.

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),
);

Het uitvoerpad moet eindigen op .pdf; de job valideert het pad op de worker voordat hij het bestand schrijft.

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]);
}
}

Push in CodeIgniter de jobHandlers-sleutel ('generate-pdf'), niet de job-klassestring. Registreer de handler eerst in 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,
];
}

Een productie-dispatch koppelt success- en failure-callbacks (Laravel), of een expliciet geregistreerde builder en een getypeerde handler (Symfony), aan een PSR-3-logger. Het Laravel-voorbeeld hieronder dispatcht met beide callbacks.

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,
]);
},
);
}
}

De success-callback ontvangt het uitvoerpad. De failure-callback ontvangt de Throwable. De job laat tries (standaard 3) verstrijken voordat het failure-pad draait. Stem timeout af via nextpdf.queue.timeout. De waarden tries en backoff zijn publieke properties, dus maak een subclass van GeneratePdfJob om ze te wijzigen.

Implementeer voor Symfony de builder en registreer die in een servicelocator. Zo blijft de handler beperkt tot geregistreerde builders.

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'

Implementeer voor CodeIgniter de builder als statische methode onder App\PdfBuilders. De job weigert elke builder-referentie buiten die namespace en elk uitvoerpad buiten 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;
}
}

Draai de worker voor elk framework.

Terminal window
php bin/console messenger:consume async --limit=200 --memory-limit=256M --time-limit=3600
Terminal window
php spark queue:work pdf-queue

Recycle Laravel- en Symfony-workers met begrensde levensduren (--limit / --memory-limit / --time-limit) zodat een gelekte allocatie in een afhankelijkheid niet onbegrensd kan groeien.

  • De geretourneerde waarde van de builder is wat wordt opgeslagen. In elke integratie slaat de worker het document op dat de builder retourneert, niet de instantie die hij aanvankelijk heeft opgelost. Retourneer altijd het geconfigureerde document vanuit de builder.
  • Padvalidatie draait op de worker. Symfony valideert het uitvoerpad bij constructie en opnieuw op het moment van consumptie. CodeIgniter beperkt het pad tot WRITEPATH/pdfs/ en weigert traversal- en sibling-prefix-paden. Een pad dat veilig was bij dispatch maar onveilig is bij consumptie, wordt nog steeds geweigerd.
  • CodeIgniter pusht de naam, niet de klasse. Als je GeneratePdfJob::class als job-naam pusht, weigert de queue die op het moment van pushen. Push in plaats daarvan de jobHandlers-sleutel.
  • Laravel-callbacks moeten worden meegegeven aan de statische dispatch. Als je een job-instantie bouwt en vervolgens $job->dispatch(...) aanroept, verwerpt die aanroep de instantie en de bijbehorende callbacks. Geef de callbacks mee aan GeneratePdfJob::dispatch(...).
  • Worker-veilige registries. De font-registry is een vergrendelde singleton met proceslevensduur en de image-registry is een begrensde cache. Documenten zijn per job vers. Vraag op de worker geen gedeeld document aan.
  • Ondertekenen in workers. Ondertekende of PDF/A-uitvoer in een queue-job vereist een commerciële NextPDF-editie die in de worker-omgeving is geïnstalleerd. Zonder die editie wordt de signing-service opgelost naar null. Voer een null-check uit voordat je ondertekent.

Door generatie naar een queued job te verplaatsen, verdwijnt de volledige PDF-bouwtijd uit de HTTP-request. De request keert terug zodra het werk is gedispatcht. De font- en image-registries spreiden hun opzetkosten over de levensduur van de worker, zodat de kosten per job beperkt blijven tot documentconstructie en content-emissie. Stem het aantal jobs in behandeling af op je worker-pool en configureer preload_fonts vooraf (Laravel, Symfony), zodat de font-warmup één keer bij het opstarten van de worker plaatsvindt in plaats van bij de eerste job.

  • Queue-payloads kunnen door aanvallers worden beïnvloed wanneer de broker bereikbaar is, dus behandel het uitvoerpad en de builder-referentie in een payload als niet-vertrouwd. De integraties dwingen dit af met padvalidatie en, in CodeIgniter, een allowlist voor de builder-namespace.
  • Beperk de bestandssysteemrechten van de worker tot de beoogde uitvoermap als verdediging in de diepte. Als een gemanipuleerd pad op de een of andere manier de validatie passeert, kan het nog steeds niet aan de map ontsnappen.
  • Log in de failure-callback de exceptie-klasse en een correlatie-identifier, nooit het bericht of de trace.
  • Schrijf nooit een leeg catch-blok. Elke failure-callback hier logt en draagt context mee.

De security-and-operations-pagina van elke integratie behandelt het volledige queue-dreigingsmodel: payloadvalidatie, callable-allowlists en padbeperking.

Deze handleiding doet geen normatieve claim over standaarden. Elke getoonde API-aanroep behoort tot het geverifieerde publieke oppervlak van de genoemde integratie. Het queued-pad steunt op container-binding-garanties: een vers document per resolutie en de vergrendelde font-registry. De upstream production-usage-pagina’s die onder Zie ook zijn gelinkt, documenteren die garanties met hun PSR-citaties. Deze cookbook-pagina herhaalt het gebruik en laat de citaties over aan die pagina’s.