Generare un PDF in un job accodato
In breve
Sezione intitolata “In breve”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/queueinstallato. - È 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.
Installazione
Sezione intitolata “Installazione”Installare l’integrazione e poi la dipendenza per la coda richiesta dal framework.
composer require nextpdf/laravelcomposer require nextpdf/symfony symfony/messengerCodeIgniter richiede il pacchetto della coda. L’integrazione lo dichiara come dipendenza riservata allo sviluppo; richiederlo quindi direttamente nell’applicazione che esegue i worker.
composer require nextpdf/codeigniter codeigniter4/queuePer Laravel, configurare la connessione di coda in config/nextpdf.php (queue.connection, queue.queue, queue.timeout) e avviare un worker per quella connessione.
Panoramica concettuale
Sezione intitolata “Panoramica concettuale”Ogni integrazione declina la stessa idea secondo le proprie convenzioni:
- Laravel fornisce
NextPDF\Laravel\Jobs\GeneratePdfJob, un jobShouldQueue. 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 messaggioreadonlyaccodato sul bus Messenger, oltre aGeneratePdfHandler, che risolve un builder dal nome della classe tramite un service locator PSR-11. ImplementareNextPDF\Symfony\Message\PdfBuilderInterfaceper ciascun tipo di documento. - CodeIgniter 4 fornisce
NextPDF\CodeIgniter\Jobs\GeneratePdfJob, registrato con una chiave denominata inConfig\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 namespaceApp\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.
Superficie dell’API
Sezione intitolata “Superficie dell’API”| Aspetto | Laravel | Symfony | CodeIgniter 4 |
|---|---|---|---|
| Unità accodata | GeneratePdfJob (ShouldQueue) | GeneratePdfMessage (DTO) + GeneratePdfHandler | GeneratePdfJob (handler della coda) |
| Accodamento | GeneratePdfJob::dispatch($path, $builder, $onSuccess, $onFailure) | MessageBusInterface::dispatch(new GeneratePdfMessage(...)) | service('queue')->push($queue, $name, $data) |
| Forma del builder | callable(PdfDocumentInterface): PdfDocumentInterface | PdfBuilderInterface::build(Document, array): Document | static fn(Document, array): Document sotto App\PdfBuilders |
| Protezione del percorso / input | Il job convalida il percorso di output sul worker | Il DTO convalida in fase di costruzione, l’handler riconvalida al consumo | Il job confina il percorso a WRITEPATH/pdfs/, applica una allowlist al namespace del builder |
| Superficie di errore | failed() dopo tries; onFailure in caso di errore terminale | Strategia di retry di Messenger; errori di convalida tipizzati | InvalidArgumentException / QueueException |
Esempio di codice — Avvio rapido
Sezione intitolata “Esempio di codice — Avvio rapido”L’accodamento minimo per ciascun 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),);Il percorso di output deve terminare con .pdf; il job convalida il percorso sul worker prima della scrittura.
<?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 va accodata la chiave jobHandlers ('generate-pdf'), non la stringa della classe del job. Registrare prima l’handler 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, ];}Esempio di codice — Produzione
Sezione intitolata “Esempio di codice — Produzione”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.
<?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.
<?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'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/.
<?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.
php bin/console messenger:consume async --limit=200 --memory-limit=256M --time-limit=3600php spark queue:work pdf-queueRiciclare 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.
Casi limite e insidie
Sezione intitolata “Casi limite e insidie”- 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::classcome nome del job viene rifiutato dalla coda al momento dell’accodamento. Accodare invece la chiavejobHandlers. - 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 aGeneratePdfJob::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 sianullprima di firmare.
Prestazioni
Sezione intitolata “Prestazioni”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.
Note sulla sicurezza
Sezione intitolata “Note sulla sicurezza”- 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
catchvuoto. 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.
Conformità
Sezione intitolata “Conformità”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.
Vedere anche
Sezione intitolata “Vedere anche”- Restituire un PDF generato da un controller — la controparte sincrona.
- Utilizzo in produzione con Laravel —
GeneratePdfJob, callback e tabella di ottimizzazione della coda. - Utilizzo in produzione con Symfony — sicurezza dei worker Messenger e il locator del builder.
- Utilizzo in produzione con CodeIgniter —
GeneratePdfJob,jobHandlerse il confinamento del percorso.