Pular para o conteúdo

Retornar um PDF gerado por um controller

Gere um arquivo Portable Document Format (PDF) em uma action de controller e retorne-o como resposta Hypertext Transfer Protocol (HTTP). Cada integração de framework inclui um helper PdfResponse que monta o objeto de resposta, define Content-Type: application/pdf, anexa os cabeçalhos de segurança e sanitiza o nome do arquivo. Este guia cobre os três modos de entrega para Laravel, Symfony e CodeIgniter 4: visualização inline, download de arquivo e entrega via stream.

Confira primeiro estes pré-requisitos, para que o caminho do controller esteja pronto antes de você começar:

  • O core do NextPDF está instalado.
  • Uma integração de framework está instalada, e o respectivo service provider, bundle ou service já foi descoberto. Confirme a descoberta na página de instalação do framework antes de começar.
  • O modo via stream não precisa de pacotes adicionais. Todas as integrações incluem a variante via stream junto com a variante com buffer.

Este é um guia prático (how-to). Ele pressupõe que você já sabe como rotear uma requisição até um controller no seu framework. Para ver um primeiro exemplo executável em cada framework, leia o quickstart do framework referenciado em Veja também.

Instale a integração do seu framework. Execute um dos comandos a seguir.

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

No Laravel, publique a configuração após a instalação.

Terminal window
php artisan vendor:publish --tag=nextpdf-config

O Symfony registra o bundle via Flex, e o CodeIgniter descobre o service automaticamente. Confirme a descoberta na página de instalação do framework antes de continuar.

Toda integração de framework segue a mesma estrutura em três partes: você obtém um documento novo, escreve conteúdo nele e o passa para uma factory PdfResponse, que retorna uma resposta HTTP. A API do documento (addPage(), cell(), setFont()) é a superfície do motor do core, e ela é idêntica em todos os frameworks. A factory de resposta difere apenas na classe de resposta retornada, porque cada framework tem seu próprio tipo de resposta HTTP.

PdfResponse oferece três modos de entrega. Inline define um cabeçalho Content-Disposition: inline, de modo que o navegador renderize o PDF em uma aba de visualização. Download define Content-Disposition: attachment, de modo que o navegador salve o arquivo. Streamed emite o corpo do PDF em blocos de tamanho fixo em vez de manter o documento inteiro em buffer na memória. Escolha-o para documentos grandes quando o pico de memória for mais importante do que um Content-Length conhecido.

Obtenha o documento pelo caminho de resolução habitual do seu framework:

  • Laravel — resolva NextPDF\Contracts\DocumentFactoryInterface a partir do container com app(...) e chame create(), que retorna um novo NextPDF\Core\Document — o tipo concreto que as factories PdfResponse aceitam.
  • Symfony — injete NextPDF\Symfony\Service\PdfFactory e chame create(), que retorna um novo NextPDF\Core\Document com os padrões de documento configurados já aplicados.
  • CodeIgniter 4 — resolva a biblioteca Pdf por meio de Services::pdf() (ou do helper pdf()), ou obtenha um documento simples por meio de pdf_document().
AspectoLaravelSymfonyCodeIgniter 4
Documento novoapp(DocumentFactoryInterface::class)->create()PdfFactory::create()pdf_document() / Services::pdf()->document()
Resposta inlinePdfResponse::inline($doc, $name)PdfResponse::inline($doc, $name)$pdf->inline($name) / PdfResponse::inline($doc, $name)
Resposta de downloadPdfResponse::download($doc, $name)PdfResponse::download($doc, $name)$pdf->download($name) / PdfResponse::download($doc, $name)
Inline via streamPdfResponse::streamInline($doc, $name)PdfResponse::streamInline($doc, $name)PdfResponse::streamInline($doc, $name)
Download via streamPdfResponse::streamDownload($doc, $name)PdfResponse::streamDownload($doc, $name)PdfResponse::streamDownload($doc, $name)
Tipo retornadoIlluminate\Http\Response (via stream: StreamedResponse)Symfony\Component\HttpFoundation\Response (via stream: StreamedResponse)CodeIgniter\HTTP\DownloadResponse

O PdfResponse do Laravel fica em NextPDF\Laravel\Http\PdfResponse, o do Symfony em NextPDF\Symfony\Http\PdfResponse, e o do CodeIgniter em NextPDF\CodeIgniter\Http\PdfResponse. A página de segurança e operações de cada integração documenta o comportamento completo de resposta daquele pacote: conjunto de cabeçalhos, regras de disposition e sanitização do nome do arquivo. Essas páginas estão listadas em Veja também.

Aqui está a action mínima de download em cada framework. As chamadas ao documento usam a mesma superfície do core. Apenas a estrutura do controller muda.

Laravel: app/Http/Controllers/ReportController.php
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Response;
use NextPDF\Contracts\DocumentFactoryInterface;
use NextPDF\Laravel\Http\PdfResponse;
final class ReportController extends Controller
{
public function download(): Response
{
$document = app(DocumentFactoryInterface::class)->create();
$document->addPage();
$document->cell(0, 10, 'Monthly report', newLine: true);
return PdfResponse::download($document, 'report.pdf');
}
}
Symfony: src/Controller/ReportController.php
<?php
declare(strict_types=1);
namespace App\Controller;
use NextPDF\Symfony\Http\PdfResponse;
use NextPDF\Symfony\Service\PdfFactory;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
final class ReportController
{
#[Route('/report', name: 'report_pdf')]
public function download(PdfFactory $pdf): Response
{
$document = $pdf->create();
$document->addPage();
$document->cell(0, 10, 'Monthly report', newLine: true);
return PdfResponse::download($document, 'report.pdf');
}
}
CodeIgniter 4: app/Controllers/ReportController.php
<?php
declare(strict_types=1);
namespace App\Controllers;
use CodeIgniter\HTTP\DownloadResponse;
use NextPDF\CodeIgniter\Config\Services;
final class ReportController extends BaseController
{
public function download(): DownloadResponse
{
$pdf = Services::pdf();
$pdf->document()->addPage();
$pdf->document()->cell(0, 10, 'Monthly report');
return $pdf->download('report.pdf');
}
}

Para visualizar no navegador em vez de baixar, troque a chamada download(...) por inline(...) no Laravel e no Symfony, ou por $pdf->inline('report.pdf') no CodeIgniter. A disposition muda para inline, e todos os demais cabeçalhos permanecem iguais.

Uma action de produção injeta suas dependências, captura a exceção mais específica documentada pela integração, registra a classe da falha sem vazar um trace e retorna um erro HTTP definido. O exemplo abaixo usa injeção via construtor do Laravel. Os equivalentes em Symfony e CodeIgniter seguem a mesma estrutura e aparecem na página de uso em produção de cada integração.

Laravel: app/Http/Controllers/InvoiceController.php
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Response;
use NextPDF\Contracts\DocumentFactoryInterface;
use NextPDF\Laravel\Http\PdfResponse;
use Psr\Log\LoggerInterface;
use Throwable;
final class InvoiceController extends Controller
{
public function __construct(
private readonly DocumentFactoryInterface $documents,
private readonly LoggerInterface $logger,
) {}
public function show(int $invoiceId): Response
{
try {
$document = $this->documents->create();
$document->addPage();
$document->cell(0, 10, "Invoice #{$invoiceId}", newLine: true);
return PdfResponse::download(
$document,
"invoice-{$invoiceId}.pdf",
);
} catch (Throwable $exception) {
// Log the exception class, never the message or a stack trace,
// so internal detail does not leak into the log sink.
$this->logger->error('Invoice PDF generation failed', [
'invoice_id' => $invoiceId,
'exception' => $exception::class,
]);
return new Response('Could not generate the invoice PDF.', 500);
}
}
}

Injete DocumentFactoryInterface e chame create() em cada action. Isso retorna um novo NextPDF\Core\Document — o tipo concreto que as factories PdfResponse do Laravel aceitam. Resolver um documento novo por requisição mantém a factory substituível em testes. Não reutilize uma instância de controller para dois documentos sem relação dentro de um único processo worker de longa duração.

Para documentos muito grandes, substitua a factory com buffer por uma via stream para limitar o pico de memória. A variante via stream retorna um StreamedResponse (Laravel e Symfony) e emite o corpo em blocos de tamanho fixo. Ela omite deliberadamente Content-Length, de modo que barras de progresso de download e proxies sensíveis ao tamanho não veem um tamanho conhecido. Prefira o download() / inline() com buffer para respostas pequenas e sensíveis à latência.

Laravel: streamed download for a large report
$document = $this->documents->create();
// ... emit content onto $document ...
return PdfResponse::streamDownload($document, 'annual-report.pdf');
  • Documento novo por chamada. Nas três integrações, o documento é um produto de factory, novo a cada resolução. Não mantenha em cache um documento resolvido entre documentos lógicos, nem entre requisições em um worker de longa duração. O estado de conteúdo obsoleto é levado adiante.
  • Nome de arquivo vazio. Um nome de arquivo vazio passado para uma factory PdfResponse recai sobre um nome padrão (document.pdf) em vez de produzir uma disposition em branco. Passe um nome de arquivo explícito e significativo.
  • Nomes de arquivo não-ASCII. A resposta do Laravel adiciona automaticamente um parâmetro RFC 5987 filename*= para nomes não-ASCII, e nomes ASCII usam o parâmetro simples. Não codifique o nome do arquivo manualmente.
  • Respostas via stream atrás de um proxy com buffer. Um proxy que mantém o corpo inteiro em buffer anula o benefício de memória do streaming. Configure o proxy para fazer streaming das respostas PDF, ou use uma resposta com buffer nesse caminho.
  • Callback via stream do Symfony. A variante via stream do Symfony retorna um StreamedResponse cujo callback faz o flush da saída. Não escreva no corpo da resposta por conta própria depois de devolvê-la.

A geração síncrona dentro de um controller bloqueia a requisição durante toda a construção do PDF. Um documento de página única costuma caber bem em um orçamento típico de requisição. Para saídas em múltiplas páginas ou em lote, mova a geração para fora da thread da requisição com um job enfileirado — veja Gerar um PDF em um job enfileirado. As variantes via stream reduzem o pico de memória para documentos grandes ao custo de um Content-Length desconhecido. Escolha-as quando a memória for a restrição e uma barra de progresso não for necessária.

  • As factories PdfResponse aplicam um conjunto fixo de cabeçalhos de fortalecimento da resposta e sanitizam o nome do arquivo de download em todas as integrações. Não adicione esses cabeçalhos por conta própria.
  • Nunca interpole entrada de usuário não validada diretamente em um nome de arquivo que você passa para a factory. Passe um valor que você controla e deixe a factory sanitizá-lo como uma segunda camada.
  • No bloco catch, registre a classe da exceção e um identificador de correlação, não a mensagem da exceção nem o trace. Um trace bruto em um destino de log representa vazamento de informação.
  • Nunca escreva um bloco catch vazio. Cada exemplo aqui registra em log e retorna uma resposta de erro definida.

A página de segurança e operações de cada integração documenta o modelo de ameaças daquela integração: conjunto de cabeçalhos, regras de sanitização do nome do arquivo e o tempo de vida do binding do documento.

Este guia não faz nenhuma alegação normativa de conformidade com padrões. Cada chamada de API mostrada é a superfície pública verificada da integração citada, conferida contra as páginas de quickstart e de uso em produção de cada pacote. As páginas de uso em produção a montante referenciadas em Veja também documentam a semântica dos cabeçalhos e o comportamento de binding no container dos quais as integrações dependem, junto com suas citações PSR. Esta página de cookbook reapresenta o uso e delega as citações normativas a essas páginas.