Pular para o conteúdo

Gerar um PDF em um job enfileirado

Geração pesada de PDF não deve rodar na thread da requisição. Cada integração de framework oferece uma API de geração enfileirada que constrói e salva um PDF em um worker. A requisição HTTP pode retornar assim que você despacha o trabalho. Este guia cobre o fluxo enfileirado para Laravel (GeneratePdfJob), Symfony (GeneratePdfMessage sobre o Messenger) e CodeIgniter 4 (GeneratePdfJob por meio de codeigniter4/queue).

Os pré-requisitos são:

  • O core do NextPDF e uma integração de framework estão instalados.
  • Um transporte de worker está configurado: uma conexão de fila do Laravel, um transporte do Symfony Messenger ou uma fila do CodeIgniter 4 com codeigniter4/queue instalado.
  • Um processo de worker está em execução para esse transporte.

Este guia presume que a aplicação já tenha uma fila. Para configurar a fila ou o Messenger, use a documentação do próprio framework.

Instale a integração e, depois, instale a dependência de fila exigida pelo framework.

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

O CodeIgniter precisa do pacote de fila. A integração o declara apenas como dependência de desenvolvimento, então adicione-o como dependência da aplicação que executa os workers.

Terminal window
composer require nextpdf/codeigniter codeigniter4/queue

No Laravel, configure a conexão de fila em config/nextpdf.php (queue.connection, queue.queue, queue.timeout) e depois execute um worker para essa conexão.

Cada integração segue o mesmo padrão, adaptado ao estilo do respectivo framework:

  • Laravel fornece NextPDF\Laravel\Jobs\GeneratePdfJob, um job ShouldQueue. Você o despacha com um caminho de saída e um closure de builder. O closure recebe um documento resolvido pelo container e retorna o documento configurado. No worker, o job salva o documento retornado nesse caminho. Ele também aceita callbacks opcionais de sucesso e de falha.
  • Symfony fornece NextPDF\Symfony\Message\GeneratePdfMessage, uma mensagem readonly despachada no barramento do Messenger, além de GeneratePdfHandler. O handler resolve um builder pelo nome da classe a partir de um service locator PSR-11. Você implementa NextPDF\Symfony\Message\PdfBuilderInterface para cada tipo de documento.
  • CodeIgniter 4 fornece NextPDF\CodeIgniter\Jobs\GeneratePdfJob, registrado sob uma chave de nome em Config\Queue::$jobHandlers. Você faz push do job pelo nome registrado, com uma referência de builder, um caminho de saída e um array de contexto. O builder é um método estático limitado ao namespace App\PdfBuilders.

As três integrações compartilham a mesma postura de segurança: todas validam o caminho de saída. Symfony e CodeIgniter o revalidam no momento do consumo, porque um payload pode ficar aguardando em uma fila entre o despacho e a execução. O builder é executado sobre um documento novo no worker, de modo que jobs concorrentes nunca compartilham o estado do documento.

AspectoLaravelSymfonyCodeIgniter 4
Unidade enfileiradaGeneratePdfJob (ShouldQueue)GeneratePdfMessage (DTO) + GeneratePdfHandlerGeneratePdfJob (handler da fila)
DespachoGeneratePdfJob::dispatch($path, $builder, $onSuccess, $onFailure)MessageBusInterface::dispatch(new GeneratePdfMessage(...))service('queue')->push($queue, $name, $data)
Formato do buildercallable(PdfDocumentInterface): PdfDocumentInterfacePdfBuilderInterface::build(Document, array): Documentstatic fn(Document, array): Document sob App\PdfBuilders
Proteção de caminho / entradaO job valida o caminho de saída no workerO DTO valida na construção; o handler revalida no consumoO job confina o caminho a WRITEPATH/pdfs/, com allowlist do namespace do builder
Superfície de falhafailed() após tries; onFailure em falha terminalEstratégia de retry do Messenger; erros de validação tipadosInvalidArgumentException / QueueException

Use este despacho mínimo em cada 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),
);

O caminho de saída deve terminar em .pdf; o job valida o caminho no worker antes de escrever o arquivo.

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

No CodeIgniter, use a chave jobHandlers ('generate-pdf'), não a string da classe do job. Registre o handler primeiro em 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,
];
}

Em produção, o despacho conecta callbacks de sucesso e de falha (Laravel), ou um builder registrado explicitamente e um handler tipado (Symfony), a um logger PSR-3. O exemplo abaixo, em Laravel, despacha com ambos os 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,
]);
},
);
}
}

O callback de sucesso recebe o caminho de saída. O callback de falha recebe o Throwable. O job esgota tries (padrão 3) antes de executar o caminho de falha. Ajuste timeout por meio de nextpdf.queue.timeout. Os valores de tries e backoff são propriedades públicas; portanto, estenda GeneratePdfJob para alterá-los.

No Symfony, implemente o builder e registre-o em um service locator. Isso mantém o handler limitado aos builders registrados.

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'

No CodeIgniter, implemente o builder como um método estático sob App\PdfBuilders. O job rejeita qualquer referência de builder fora desse namespace e qualquer caminho de saída fora de 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;
}
}

Execute o worker para cada 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

Recicle os workers do Laravel e do Symfony com ciclos de vida limitados (--limit / --memory-limit / --time-limit) para que um vazamento de memória em uma dependência não possa crescer sem limite.

  • O valor de retorno do builder é o que é salvo. Em todas as integrações, o worker salva o documento que o builder retorna, não a instância resolvida originalmente. Sempre retorne o documento configurado pelo builder.
  • A validação de caminho é executada no worker. O Symfony valida o caminho de saída na construção e novamente no momento do consumo. O CodeIgniter confina o caminho a WRITEPATH/pdfs/ e rejeita caminhos de traversal e de prefixo-irmão. Um caminho que era seguro no despacho, mas inseguro no consumo, ainda é rejeitado.
  • O CodeIgniter usa o nome, não a classe. Se você passar GeneratePdfJob::class como nome do job, a fila o rejeita no momento do push. Use a chave jobHandlers em vez disso.
  • Os callbacks do Laravel devem ser passados para o dispatch estático. Se você criar uma instância do job e então chamar $job->dispatch(...), essa chamada descarta a instância e seus callbacks. Passe os callbacks para GeneratePdfJob::dispatch(...).
  • Registries seguros para workers. O registry de fontes é um singleton travado pelo ciclo de vida do processo, e o registry de imagens é um cache limitado. Os documentos são novos por job. Não solicite um documento compartilhado no worker.
  • Assinatura em workers. A saída assinada ou PDF/A em um job de fila requer uma edição comercial do NextPDF instalada no ambiente do worker. Sem ela, o serviço de assinatura resolve para null. Faça a verificação de null antes de assinar.

Mover a geração para um job enfileirado retira da requisição HTTP todo o tempo de construção do PDF. A requisição retorna assim que o trabalho é despachado. Os registries de fontes e de imagens amortizam o custo de configuração ao longo do ciclo de vida do worker, de modo que o custo por job fica restrito à construção do documento e à emissão de conteúdo. Dimensione o número de jobs em andamento conforme o pool de workers e pré-popule preload_fonts (Laravel, Symfony) para que o aquecimento das fontes ocorra uma vez na inicialização do worker, em vez de no primeiro job.

  • Os payloads de fila podem ser influenciados por atacantes quando o broker é acessível, então trate o caminho de saída e a referência de builder em um payload como não confiáveis. As integrações impõem isso com validação de caminho e, no CodeIgniter, com uma allowlist de namespace do builder.
  • Restrinja as permissões do sistema de arquivos do worker ao diretório de saída pretendido como defesa em profundidade. Se um caminho adulterado de algum modo passar pela validação, ele ainda não consegue escapar do diretório.
  • Registre a classe da exceção e um identificador de correlação no callback de falha, nunca a mensagem ou o trace.
  • Nunca escreva um bloco catch vazio. Cada callback de falha aqui registra logs e inclui contexto.

A página de segurança e operações de cada integração cobre o modelo de ameaças completo da fila: validação de payload, allowlists de callables e confinamento de caminho.

Este guia não faz nenhuma afirmação normativa sobre padrões. Cada chamada de API mostrada pertence à superfície pública verificada da integração nomeada. O caminho enfileirado depende de garantias de binding do container: um documento novo por resolução e o registry de fontes travado. As páginas upstream de uso em produção listadas em Veja também documentam essas garantias com suas citações PSR. Esta página de cookbook reafirma o uso e delega as citações a essas páginas.