Generar un PDF desde un trabajo en cola
De un vistazo
Sección titulada «De un vistazo»La generación pesada de PDF no debe ejecutarse en el hilo de la solicitud. Cada integración de framework ofrece una superficie para generar PDF en cola, que construye y guarda un PDF en un worker, de modo que la solicitud HTTP se completa en cuanto se despacha el trabajo. Esta guía cubre la vía en cola para Laravel (GeneratePdfJob), Symfony (GeneratePdfMessage sobre Messenger) y CodeIgniter 4 (GeneratePdfJob a través de codeigniter4/queue).
Los prerrequisitos son:
- El core de NextPDF y una integración de framework deben estar instalados.
- Debe haber un transporte de worker configurado: una conexión de cola de Laravel, un transporte de Symfony Messenger o una cola de CodeIgniter 4 con
codeigniter4/queueinstalado. - Debe haber un proceso de worker en ejecución para ese transporte.
Esta guía asume una aplicación que ya tiene una cola configurada. Para configurar la cola o Messenger, consultar la documentación del propio framework.
Instalación
Sección titulada «Instalación»Instalar la integración y, a continuación, la dependencia de cola que requiere el framework.
composer require nextpdf/laravelcomposer require nextpdf/symfony symfony/messengerCodeIgniter necesita el paquete de cola. La integración lo declara solo como dependencia de desarrollo; por eso, hay que requerirlo directamente en la aplicación que ejecuta los workers.
composer require nextpdf/codeigniter codeigniter4/queuePara Laravel, configurar la conexión de cola en config/nextpdf.php (queue.connection, queue.queue, queue.timeout) y ejecutar un worker para esa conexión.
Panorama conceptual
Sección titulada «Panorama conceptual»Cada integración expresa la misma idea a su manera:
- Laravel incluye
NextPDF\Laravel\Jobs\GeneratePdfJob, un job de tipoShouldQueue. Se despacha con una ruta de salida y un closure de builder. El closure recibe un documento resuelto por el contenedor y devuelve el documento configurado. El job guarda ese documento devuelto en la ruta dentro del worker. También acepta callbacks opcionales de éxito y fallo. - Symfony incluye
NextPDF\Symfony\Message\GeneratePdfMessage, un mensajereadonlydespachado mediante el bus de Messenger, junto conGeneratePdfHandler, que resuelve un builder por su nombre de clase desde un service locator PSR-11. Para cada tipo de documento, se implementaNextPDF\Symfony\Message\PdfBuilderInterface. - CodeIgniter 4 incluye
NextPDF\CodeIgniter\Jobs\GeneratePdfJob, registrado con una clave de nombre enConfig\Queue::$jobHandlers. Se encola el job por su nombre registrado con una referencia de builder, una ruta de salida y un array de contexto. El builder es un método estático confinado al namespaceApp\PdfBuilders.
Las tres comparten la misma postura de seguridad: se valida la ruta de salida. Symfony y CodeIgniter la vuelven a validar en el momento del consumo, porque un payload puede permanecer en una cola entre el despacho y la ejecución. El builder se ejecuta sobre un documento nuevo en el worker, de modo que los trabajos concurrentes nunca comparten estado del documento.
Superficie de la API
Sección titulada «Superficie de la API»| Aspecto | Laravel | Symfony | CodeIgniter 4 |
|---|---|---|---|
| Unidad en cola | GeneratePdfJob (ShouldQueue) | GeneratePdfMessage (DTO) + GeneratePdfHandler | GeneratePdfJob (handler de cola) |
| Despacho | 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 bajo App\PdfBuilders |
| Validación de ruta / entrada | El job valida la ruta de salida en el worker | El DTO valida en la construcción; el handler vuelve a validar en el consumo | El job confina la ruta a WRITEPATH/pdfs/ y aplica una allowlist al namespace del builder |
| Superficie de fallo | failed() tras tries; onFailure en el fallo terminal | Estrategia de reintentos de Messenger; errores de validación tipados | InvalidArgumentException / QueueException |
Ejemplo de código — Inicio rápido
Sección titulada «Ejemplo de código — Inicio rápido»Despacho mínimo en cada 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),);La ruta de salida debe terminar en .pdf; el job valida la ruta en el worker antes de escribir el archivo.
<?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]); }}En CodeIgniter, encolar la clave de jobHandlers ('generate-pdf'), no la cadena de clase del job. Primero, registrar el handler en 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, ];}Ejemplo de código — Producción
Sección titulada «Ejemplo de código — Producción»En producción, el despacho conecta los callbacks de éxito y fallo (Laravel), o un builder registrado explícitamente y un handler tipado (Symfony), y registra todo a través de un logger PSR-3. El ejemplo de Laravel a continuación despacha con ambos 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, ]); }, ); }}El callback de éxito recibe la ruta de salida; el callback de fallo recibe el Throwable. El job agota tries (de forma predeterminada 3) antes de ejecutar la vía de fallo. Ajustar timeout mediante nextpdf.queue.timeout. Como tries y backoff son propiedades públicas, crear una subclase de GeneratePdfJob para cambiarlos.
Para Symfony, implementar el builder y registrarlo en un service locator de modo que solo los builders registrados sean alcanzables desde el handler.
<?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'Para CodeIgniter, implementar el builder como un método estático bajo App\PdfBuilders. El job rechaza cualquier referencia de builder fuera de ese namespace y cualquier ruta de salida fuera de 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; }}Ejecutar el worker para cada framework.
php bin/console messenger:consume async --limit=200 --memory-limit=256M --time-limit=3600php spark queue:work pdf-queueReciclar los workers de Laravel y Symfony con tiempos de vida acotados (--limit / --memory-limit / --time-limit) para que una fuga de memoria en una dependencia no pueda crecer sin límite.
Casos límite y trampas
Sección titulada «Casos límite y trampas»- El valor que devuelve el builder es lo que se guarda. En todas las integraciones, el worker guarda el documento que devuelve el builder, no la instancia resuelta originalmente. Devolver siempre el documento configurado desde el builder.
- La validación de la ruta se ejecuta en el worker. Symfony valida la ruta de salida en la construcción y de nuevo en el momento del consumo. CodeIgniter confina la ruta a
WRITEPATH/pdfs/y rechaza rutas con traversal y rutas de prefijo hermano. Una ruta segura en el despacho pero insegura en el consumo se rechaza igualmente. - CodeIgniter encola el nombre, no la clase. La cola rechaza en el momento del encolado el uso de
GeneratePdfJob::classcomo nombre del job. En su lugar, encolar la clave dejobHandlers. - Los callbacks de Laravel deben pasarse al dispatch estático. Construir una instancia del job y luego llamar a
$job->dispatch(...)descarta esa instancia y sus callbacks. Pasar los callbacks aGeneratePdfJob::dispatch(...). - Registries seguros para el worker. El registry de fuentes es un singleton bloqueado durante la vida del proceso, y el registry de imágenes es una caché acotada. Los documentos son nuevos en cada job. No solicitar un documento compartido en el worker.
- Firma en los workers. La salida firmada o PDF/A en un trabajo en cola requiere una edición comercial de NextPDF instalada en el entorno del worker; sin ella, el servicio de firma se resuelve como
null. Comprobar si esnullantes de firmar.
Rendimiento
Sección titulada «Rendimiento»Mover la generación a un trabajo en cola elimina de la solicitud HTTP todo el tiempo de construcción del PDF: la solicitud se completa en cuanto se despacha el trabajo. Los registries de fuentes y de imágenes amortizan su coste de preparación durante la vida del worker, de modo que el coste por trabajo se limita a la construcción del documento y a la emisión del contenido. Dimensionar el número de trabajos en vuelo según el pool de workers y definir preload_fonts (Laravel, Symfony) para que el calentamiento de fuentes ocurra una sola vez al arrancar el worker, en lugar de hacerlo en el primer trabajo.
Notas de seguridad
Sección titulada «Notas de seguridad»- Los payloads de la cola pueden estar influidos por un atacante cuando el broker es accesible; por eso, tratar la ruta de salida y la referencia de builder de un payload como no confiables. Las integraciones imponen esta regla con validación de ruta y, en CodeIgniter, una allowlist de namespace de builder.
- Restringir los permisos del sistema de archivos del worker al directorio de salida previsto como defensa en profundidad, de modo que una ruta manipulada que de algún modo pase la validación aún no pueda escapar del directorio.
- Registrar la clase de la excepción y un identificador de correlación en el callback de fallo, nunca el mensaje ni la traza.
- Nunca escribir un bloque
catchvacío. Cada callback de fallo aquí registra y aporta contexto.
El modelo de amenazas completo de la cola —validación de payload, allowlists de callables y confinamiento de rutas— se describe en la página de seguridad y operaciones de cada integración.
Conformidad
Sección titulada «Conformidad»Esta guía no formula ninguna afirmación normativa sobre estándares. Cada llamada a la API que se muestra corresponde a la superficie pública verificada de la integración nombrada. Las garantías de enlace al contenedor de las que depende la vía en cola (un documento nuevo por cada resolución, el registry de fuentes bloqueado) están documentadas con sus citas PSR en las páginas de uso en producción enlazadas en Véase también. Esta página del cookbook reformula el uso y delega las citas en esas páginas.
Véase también
Sección titulada «Véase también»- Devolver un PDF generado desde un controlador — la contraparte síncrona.
- Uso en producción de Laravel —
GeneratePdfJob, callbacks y la tabla de ajuste de la cola. - Uso en producción de Symfony — seguridad del worker de Messenger y el locator de builders.
- Uso en producción de CodeIgniter —
GeneratePdfJob,jobHandlersy confinamiento de rutas.