Gerar um PDF em um job enfileirado
Visão geral
Seção intitulada “Visão geral”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/queueinstalado. - 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.
Instalação
Seção intitulada “Instalação”Instale a integração e, depois, instale a dependência de fila exigida pelo framework.
composer require nextpdf/laravelcomposer require nextpdf/symfony symfony/messengerO 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.
composer require nextpdf/codeigniter codeigniter4/queueNo 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.
Visão conceitual
Seção intitulada “Visão conceitual”Cada integração segue o mesmo padrão, adaptado ao estilo do respectivo framework:
- Laravel fornece
NextPDF\Laravel\Jobs\GeneratePdfJob, um jobShouldQueue. 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 mensagemreadonlydespachada no barramento do Messenger, além deGeneratePdfHandler. O handler resolve um builder pelo nome da classe a partir de um service locator PSR-11. Você implementaNextPDF\Symfony\Message\PdfBuilderInterfacepara cada tipo de documento. - CodeIgniter 4 fornece
NextPDF\CodeIgniter\Jobs\GeneratePdfJob, registrado sob uma chave de nome emConfig\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 namespaceApp\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.
Superfície da API
Seção intitulada “Superfície da API”| Aspecto | Laravel | Symfony | CodeIgniter 4 |
|---|---|---|---|
| Unidade enfileirada | GeneratePdfJob (ShouldQueue) | GeneratePdfMessage (DTO) + GeneratePdfHandler | GeneratePdfJob (handler da fila) |
| Despacho | GeneratePdfJob::dispatch($path, $builder, $onSuccess, $onFailure) | MessageBusInterface::dispatch(new GeneratePdfMessage(...)) | service('queue')->push($queue, $name, $data) |
| Formato do builder | callable(PdfDocumentInterface): PdfDocumentInterface | PdfBuilderInterface::build(Document, array): Document | static fn(Document, array): Document sob App\PdfBuilders |
| Proteção de caminho / entrada | O job valida o caminho de saída no worker | O DTO valida na construção; o handler revalida no consumo | O job confina o caminho a WRITEPATH/pdfs/, com allowlist do namespace do builder |
| Superfície de falha | failed() após tries; onFailure em falha terminal | Estratégia de retry do Messenger; erros de validação tipados | InvalidArgumentException / QueueException |
Exemplo de código — Início rápido
Seção intitulada “Exemplo de código — Início rápido”Use este despacho mínimo em 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),);O caminho de saída deve terminar em .pdf; o job valida o caminho no worker antes de escrever o arquivo.
<?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]); }}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.
<?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, ];}Exemplo de código — Produção
Seção intitulada “Exemplo de código — Produção”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.
<?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.
<?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'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/.
<?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.
php bin/console messenger:consume async --limit=200 --memory-limit=256M --time-limit=3600php spark queue:work pdf-queueRecicle 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.
Casos extremos & armadilhas
Seção intitulada “Casos extremos & armadilhas”- 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::classcomo nome do job, a fila o rejeita no momento do push. Use a chavejobHandlersem 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 paraGeneratePdfJob::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.
Desempenho
Seção intitulada “Desempenho”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.
Notas de segurança
Seção intitulada “Notas de segurança”- 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
catchvazio. 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.
Conformidade
Seção intitulada “Conformidade”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.
Veja também
Seção intitulada “Veja também”- Retorne um PDF gerado a partir de um controller — a contraparte síncrona.
- Uso em produção no Laravel —
GeneratePdfJob, callbacks e a tabela de ajustes de fila. - Uso em produção no Symfony — segurança do worker do Messenger e o builder locator.
- Uso em produção no CodeIgniter —
GeneratePdfJob,jobHandlerse confinamento de caminho.