Salta ai contenuti

Generare un PDF in un job accodato

La generazione onerosa di PDF non deve avvenire nel thread di richiesta. Ogni integrazione del framework fornisce un’interfaccia di generazione accodata che costruisce e salva un PDF su un worker, così la risposta HTTP può essere restituita non appena il job viene accodato. Questa guida descrive il flusso accodato per Laravel (GeneratePdfJob), Symfony (GeneratePdfMessage tramite Messenger) e CodeIgniter 4 (GeneratePdfJob tramite codeigniter4/queue).

I prerequisiti sono:

  • Il core NextPDF e un’integrazione del framework sono installati.
  • È configurato un trasporto per i worker: una connessione di coda Laravel, un trasporto Symfony Messenger oppure una coda CodeIgniter 4 con il pacchetto codeigniter4/queue installato.
  • È attivo un processo worker per quel trasporto.

Questa guida presuppone che l’applicazione disponga già di una coda. Per configurare la coda o Messenger, consultare la documentazione specifica del framework.

Installare l’integrazione e poi la dipendenza per la coda richiesta dal framework.

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

CodeIgniter richiede il pacchetto della coda. L’integrazione lo dichiara come dipendenza riservata allo sviluppo; richiederlo quindi direttamente nell’applicazione che esegue i worker.

Terminal window
composer require nextpdf/codeigniter codeigniter4/queue

Per Laravel, configurare la connessione di coda in config/nextpdf.php (queue.connection, queue.queue, queue.timeout) e avviare un worker per quella connessione.

Ogni integrazione declina la stessa idea secondo le proprie convenzioni:

  • Laravel fornisce NextPDF\Laravel\Jobs\GeneratePdfJob, un job ShouldQueue. Lo si accoda con un percorso di output e una closure builder. La closure riceve un documento risolto dal container e restituisce il documento configurato. Il job salva nel worker il documento restituito nel percorso indicato. Accetta inoltre callback facoltativi di successo e di errore.
  • Symfony fornisce NextPDF\Symfony\Message\GeneratePdfMessage, un messaggio readonly accodato sul bus Messenger, oltre a GeneratePdfHandler, che risolve un builder dal nome della classe tramite un service locator PSR-11. Implementare NextPDF\Symfony\Message\PdfBuilderInterface per ciascun tipo di documento.
  • CodeIgniter 4 fornisce NextPDF\CodeIgniter\Jobs\GeneratePdfJob, registrato con una chiave denominata in Config\Queue::$jobHandlers. Il job si accoda tramite il nome registrato, con un riferimento al builder, un percorso di output e un array di contesto. Il builder è un metodo statico confinato al namespace App\PdfBuilders.

Tutte e tre condividono la stessa impostazione di sicurezza: il percorso di output viene convalidato. Symfony e CodeIgniter lo riconvalidano al momento del consumo, perché un payload può rimanere in coda tra l’accodamento e l’esecuzione. Il builder opera su un documento nuovo nel worker, quindi i job concorrenti non condividono mai lo stato del documento.

AspettoLaravelSymfonyCodeIgniter 4
Unità accodataGeneratePdfJob (ShouldQueue)GeneratePdfMessage (DTO) + GeneratePdfHandlerGeneratePdfJob (handler della coda)
AccodamentoGeneratePdfJob::dispatch($path, $builder, $onSuccess, $onFailure)MessageBusInterface::dispatch(new GeneratePdfMessage(...))service('queue')->push($queue, $name, $data)
Forma del buildercallable(PdfDocumentInterface): PdfDocumentInterfacePdfBuilderInterface::build(Document, array): Documentstatic fn(Document, array): Document sotto App\PdfBuilders
Protezione del percorso / inputIl job convalida il percorso di output sul workerIl DTO convalida in fase di costruzione, l’handler riconvalida al consumoIl job confina il percorso a WRITEPATH/pdfs/, applica una allowlist al namespace del builder
Superficie di errorefailed() dopo tries; onFailure in caso di errore terminaleStrategia di retry di Messenger; errori di convalida tipizzatiInvalidArgumentException / QueueException

L’accodamento minimo per ciascun 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),
);

Il percorso di output deve terminare con .pdf; il job convalida il percorso sul worker prima della scrittura.

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

In CodeIgniter va accodata la chiave jobHandlers ('generate-pdf'), non la stringa della classe del job. Registrare prima l’handler 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,
];
}

In produzione, un accodamento collega callback di successo e di errore (Laravel), oppure un builder registrato esplicitamente e un handler tipizzato (Symfony), e scrive i log tramite un logger PSR-3. L’esempio Laravel seguente accoda il job con entrambi i callback.

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

Il callback di successo riceve il percorso di output; il callback di errore riceve il Throwable. Il job esaurisce i tentativi tries (valore predefinito 3) prima di eseguire il flusso di errore. Regolare timeout tramite nextpdf.queue.timeout. I valori tries e backoff sono proprietà pubbliche, quindi creare una sottoclasse di GeneratePdfJob per modificarli.

In Symfony, implementare il builder e registrarlo in un service locator in modo che l’handler possa raggiungere solo i builder registrati.

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'

In CodeIgniter, implementare il builder come metodo statico sotto App\PdfBuilders. Il job rifiuta qualsiasi riferimento al builder esterno a quel namespace e qualsiasi percorso di output esterno a 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;
}
}

Eseguire il worker per ciascun 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

Riciclare i worker Laravel e Symfony con tempi di vita limitati (--limit / --memory-limit / --time-limit) in modo che una perdita di memoria in una dipendenza non possa crescere senza limiti.

  • Ciò che viene salvato è il valore restituito dal builder. In ogni integrazione, il worker salva il documento restituito dal builder, non l’istanza risolta inizialmente. Restituire sempre il documento configurato dal builder.
  • La convalida del percorso viene eseguita sul worker. Symfony convalida il percorso di output alla costruzione e nuovamente al momento del consumo. CodeIgniter confina il percorso a WRITEPATH/pdfs/ e rifiuta i percorsi di traversal e con prefisso fratello. Un percorso che era sicuro all’accodamento ma non sicuro al consumo viene comunque rifiutato.
  • CodeIgniter accoda il nome, non la classe. L’accodamento di GeneratePdfJob::class come nome del job viene rifiutato dalla coda al momento dell’accodamento. Accodare invece la chiave jobHandlers.
  • I callback di Laravel devono essere passati al dispatch statico. Costruire un’istanza del job e poi chiamare $job->dispatch(...) scarta quell’istanza e i suoi callback. Passare i callback a GeneratePdfJob::dispatch(...).
  • Registry sicuri per i worker. Il registry dei font è un singleton bloccato con durata pari a quella del processo, mentre il registry delle immagini è una cache limitata. I documenti sono nuovi per ogni job. Non richiedere un documento condiviso nel worker.
  • Firma nei worker. L’output firmato o PDF/A in un job di coda richiede un’edizione commerciale di NextPDF installata nell’ambiente del worker; senza di essa, il servizio di firma si risolve in null. Verificare che non sia null prima di firmare.

Spostare la generazione in un job accodato elimina l’intero tempo di costruzione del PDF dalla richiesta HTTP: la risposta viene restituita non appena il job viene accodato. I registry dei font e delle immagini ammortizzano il costo di inizializzazione lungo la durata del worker, quindi il costo per job è limitato alla costruzione del documento e all’emissione del contenuto. Dimensionare il numero di job in corso rispetto al pool di worker e pre-popolare preload_fonts (Laravel, Symfony) in modo che il riscaldamento dei font avvenga una sola volta all’avvio del worker anziché al primo job.

  • I payload della coda possono essere influenzati da un utente malintenzionato quando il broker è raggiungibile, quindi considerare il percorso di output e il riferimento al builder in un payload come non attendibili. Le integrazioni applicano questo principio con la convalida del percorso e, in CodeIgniter, con una allowlist del namespace del builder.
  • Limitare i permessi del file system del worker alla directory di output prevista come difesa in profondità, in modo che un percorso manomesso che in qualche modo superi la convalida non possa comunque uscire dalla directory.
  • Registrare la classe dell’eccezione e un identificatore di correlazione nel callback di errore, mai il messaggio o la traccia.
  • Non scrivere mai un blocco catch vuoto. Ogni callback di errore mostrato qui registra e trasporta il contesto.

Il modello di minaccia completo della coda — convalida del payload, allowlist dei callable e confinamento del percorso — si trova nella pagina di sicurezza e operazioni di ciascuna integrazione.

Questa guida non formula alcuna dichiarazione di conformità a standard normativi. Ogni chiamata API mostrata corrisponde alla superficie pubblica verificata dell’integrazione indicata. Le garanzie di binding del container su cui si basa il percorso accodato (un documento nuovo per ogni risoluzione, il registry dei font bloccato) sono documentate con le relative citazioni PSR nelle pagine di utilizzo in produzione a monte, collegate sotto Vedere anche. Questa pagina del cookbook riprende l’utilizzo e rimanda le citazioni a quelle pagine.