Pular para o conteúdo

Transmita um PDF grande gerado como uma resposta HTTP

Você gera um PDF grande dentro de um controller e quer retornar os bytes sem manter uma segunda cópia completa no buffer da resposta. Cada integração de framework oferece variantes transmitidas em sua factory PdfResponse: streamInline() e streamDownload(). Cada método retorna um StreamedResponse do framework com um callback que escreve o corpo do PDF para o cliente em fragmentos fixos de 64 KB.

Entenda o modelo de memória antes de escolher este caminho. O engine constrói primeiro o documento completo na memória. O callback de transmissão chama getPdfData(), que materializa o PDF inteiro como uma única string e, em seguida, percorre essa string em fatias de 64 KB. Você evita o custo de pico da segunda cópia que um Illuminate\Http\Response ou Symfony\Component\HttpFoundation\Response em buffer manteria enquanto o framework mede o Content-Length. A variante transmitida não mede o comprimento, portanto omite o Content-Length. Ela nunca mantém o corpo da resposta e a string do documento ao mesmo tempo. Isso não é transmissão incremental verdadeira: o NextPDF não tem uma interface de escrita incremental, então o documento é totalmente materializado antes que o primeiro byte chegue ao socket.

Antes de começar, confirme que estes itens estejam no lugar:

  • O NextPDF core está instalado, e uma integração de framework, nextpdf/laravel ou nextpdf/symfony, também está instalada e descoberta.
  • Você já sabe como rotear uma requisição para um controller no seu framework.
  • Você leu Retorne um PDF gerado de um controller, que aborda as factories em buffer inline() e download() sobre as quais esta receita se baseia.

Este guia prático se concentra no padrão StreamedResponse compartilhado por Laravel e Symfony. O CodeIgniter 4 oferece os mesmos nomes de método streamInline() / streamDownload(), mas envolve os bytes em um CodeIgniter\HTTP\DownloadResponse em vez de um StreamedResponse orientado a callback. A seção Casos extremos aborda essa diferença.

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

Para 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. Confirme se ele foi descoberto na página de instalação do seu framework antes de continuar.

Uma factory de resposta em buffer, PdfResponse::download() ou PdfResponse::inline(), chama getPdfData(), armazena a string retornada em um objeto Response e define o Content-Length a partir de strlen(). Em seguida, o framework mantém essa string durante todo o ciclo de vida da resposta. Para um documento grande, a string do documento e a string do corpo da resposta permanecem na memória ao mesmo tempo.

A factory de transmissão usa uma abordagem diferente. PdfResponse::streamDownload() e PdfResponse::streamInline() retornam um StreamedResponse construído com um callback. O framework invoca esse callback apenas quando está pronto para enviar o corpo. Dentro do callback, a integração chama getPdfData() uma vez, divide a string retornada em fragmentos de 64 KB e faz echo de cada fragmento seguido por um flush(). Ela não retém uma segunda cópia persistente do corpo e não emite um cabeçalho Content-Length.

Dois fatos moldam cada decisão nesta página:

  • A construção é imediata, a transferência é fragmentada. getPdfData() em NextPDF\Core\Document chama o writer e retorna o PDF inteiro como uma única string. A fragmentação de 64 KB controla apenas como os bytes já construídos deixam o processo. O pico de memória é limitado pelo tamanho de um documento finalizado, não por uma pequena janela de transmissão.
  • Sem Content-Length. A variante transmitida não tem como saber o comprimento do corpo sem construí-lo dentro do callback, então ela omite o cabeçalho. Uma barra de progresso do cliente, uma requisição Range ou um proxy sensível ao comprimento não terão acesso a um tamanho. Escolha o download() / inline() em buffer quando um comprimento conhecido for mais importante do que economizar a cópia da resposta.

Obtenha o documento pelo caminho de resolução idiomático do framework:

  • Laravel: resolva NextPDF\Contracts\DocumentFactoryInterface a partir do container e chame create(). O método retorna um novo NextPDF\Core\Document, o tipo concreto que as factories de transmissão aceitam.
  • Symfony: injete NextPDF\Symfony\Service\PdfFactory e chame create(). Ela retorna um novo NextPDF\Core\Document com os padrões configurados aplicados.
AspectoLaravelSymfony
Documento novoapp(DocumentFactoryInterface::class)->create()PdfFactory::create()
Inline transmitidoPdfResponse::streamInline($doc, $name)PdfResponse::streamInline($doc, $name)
Download transmitidoPdfResponse::streamDownload($doc, $name)PdfResponse::streamDownload($doc, $name)
Tipo retornadoSymfony\Component\HttpFoundation\StreamedResponseSymfony\Component\HttpFoundation\StreamedResponse
Chamada de construção dentro do callbackNextPDF\Core\Document::getPdfData()NextPDF\Core\Document::getPdfData()
Tamanho do fragmento64 KB (str_split determinístico)64 KB (loop substr determinístico)

O PdfResponse do Laravel fica em NextPDF\Laravel\Http\PdfResponse; o do Symfony fica em NextPDF\Symfony\Http\PdfResponse. As factories de transmissão de ambos retornam o mesmo tipo Symfony\Component\HttpFoundation\StreamedResponse. Ambos aplicam o mesmo conjunto fixo de cabeçalhos de fortalecimento de resposta do Open Web Application Security Project (OWASP) (X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Content-Security-Policy: default-src 'none', X-Robots-Tag: noindex, nofollow, Referrer-Policy: no-referrer), e ambos sanitizam o nome do arquivo de download. Você não precisa adicionar esses cabeçalhos por conta própria.

Ambas as factories chamam a mesma API subjacente do core, NextPDF\Core\Document::getPdfData(): string, que constrói e retorna todo o binário do PDF. Seu equivalente save(string $path): void escreve os mesmos bytes em disco por meio de um writer atômico. Esta receita usa getPdfData() porque o destino é um socket HTTP, não um arquivo.

Aqui está a action mínima de download transmitido em cada framework. As chamadas de documento usam a mesma API do core; apenas a estrutura do controller difere. A factory de transmissão entrega um callback ao framework, então a action retorna imediatamente. O corpo é construído e descarregado quando o framework envia a resposta.

Laravel: app/Http/Controllers/ReportController.php
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use NextPDF\Contracts\DocumentFactoryInterface;
use NextPDF\Laravel\Http\PdfResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
final class ReportController extends Controller
{
public function annualReport(): StreamedResponse
{
$document = app(DocumentFactoryInterface::class)->create();
$document->addPage();
$document->cell(0, 10, 'Annual report', newLine: true);
return PdfResponse::streamDownload($document, 'annual-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\StreamedResponse;
use Symfony\Component\Routing\Attribute\Route;
final class ReportController
{
#[Route('/report', name: 'report_pdf')]
public function annualReport(PdfFactory $pdf): StreamedResponse
{
$document = $pdf->create();
$document->addPage();
$document->cell(0, 10, 'Annual report', newLine: true);
return PdfResponse::streamDownload($document, 'annual-report.pdf');
}
}

Para visualizar em uma aba do navegador em vez de forçar um download, chame streamInline(...) no lugar de streamDownload(...). O Content-Disposition passa a ser inline, e todos os outros cabeçalhos permanecem iguais.

Uma action de produção injeta suas dependências, valida a entrada vinda da rota, captura a exceção mais específica que a construção pode lançar, registra a classe da falha sem vazar um trace e retorna um erro definido de Hypertext Transfer Protocol (HTTP). O exemplo abaixo usa a injeção via construtor do Laravel. O equivalente em Symfony segue o mesmo formato, com PdfFactory injetado na action.

getPdfData() é executado dentro do callback de transmissão, então uma exceção lançada por ele aparece depois que o framework já começou a enviar os cabeçalhos. Para manter o tratamento de erros útil, construa o documento (a etapa que pode falhar) antes de devolver a resposta e capture a falha de construção nesse ponto. Assim, apenas a transferência fragmentada de bytes já construídos ocorre dentro do callback.

Laravel: app/Http/Controllers/StatementController.php
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Illuminate\Http\Response;
use NextPDF\Contracts\DocumentFactoryInterface;
use NextPDF\Core\Document;
use NextPDF\Exception\NextPdfException;
use NextPDF\Laravel\Http\PdfResponse;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\StreamedResponse;
final class StatementController extends Controller
{
private const int MAX_STATEMENT_ID = 9_999_999;
public function __construct(
private readonly DocumentFactoryInterface $documents,
private readonly LoggerInterface $logger,
) {}
public function show(int $statementId): StreamedResponse|Response
{
// Validate input at the boundary before any build work runs.
if ($statementId < 1 || $statementId > self::MAX_STATEMENT_ID) {
return new Response('Invalid statement identifier.', 422);
}
try {
// Build the whole document up front. getPdfData(), invoked inside
// the streamed callback, materializes the full PDF in memory, so
// do the failure-prone build here, where the catch can still set a
// clean HTTP status before any byte is sent.
$document = $this->buildStatement($statementId);
$document->getPdfData();
} catch (NextPdfException $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('Statement PDF build failed', [
'statement_id' => $statementId,
'exception' => $exception::class,
]);
return new Response('Could not generate the statement PDF.', 500);
}
// The build succeeded. The streamed factory rebuilds the bytes inside
// its callback and flushes them to the client in 64 KB chunks.
return PdfResponse::streamDownload(
$document,
"statement-{$statementId}.pdf",
);
}
private function buildStatement(int $statementId): Document
{
$document = $this->documents->create();
$document->addPage();
$document->cell(0, 10, "Statement #{$statementId}", newLine: true);
return $document;
}
}

Capture NextPDF\Exception\NextPdfException, a base abstrata estendida por toda exceção do NextPDF, quando você quiser um único handler para qualquer falha de construção. Para responder a causas específicas, capture primeiro os subtipos concretos que getPdfData() pode lançar: NextPDF\Exception\PageLayoutException quando o conteúdo não cabe na geometria da página, NextPDF\Exception\CompressionException quando a compressão de stream falha e NextPDF\Exception\InvalidConfigException para uma configuração de saída inválida. Nunca escreva um bloco catch vazio. Cada ramo aqui registra a classe da falha e retorna um status definido.

Resolver um documento novo por action mantém a factory substituível em testes. Não reutilize uma instância de controller para dois documentos não relacionados dentro de um único processo de worker de longa duração, porque o estado de conteúdo obsoleto é carregado adiante.

  • O documento é construído duas vezes no padrão “validar e depois transmitir”. O exemplo de produção chama getPdfData() uma vez para validar a construção e, em seguida, a factory o chama novamente dentro do callback. Esse é o custo de mover o ponto de falha para antes dos cabeçalhos. Quando uma construção dupla for cara demais para um determinado documento, pule a sondagem de pré-construção e aceite que uma falha de construção dentro do callback trunque uma resposta já iniciada.
  • Sem Content-Length. A variante transmitida omite o cabeçalho. Barras de progresso de download e requisições Range não funcionarão. Use o download() / inline() em buffer quando um comprimento conhecido for necessário.
  • Um proxy que faz buffer anula o benefício. Um proxy reverso ou um buffer de saída do PHP que captura o corpo inteiro antes de encaminhá-lo mantém o PDF completo novamente, o que anula a economia de cópia. Configure o proxy para transmitir respostas application/pdf, ou use uma resposta em buffer nesse caminho.
  • O CodeIgniter 4 não usa callback para transmissão. A integração do CodeIgniter oferece os mesmos nomes de método streamInline() / streamDownload(), mas eles retornam um CodeIgniter\HTTP\DownloadResponse que mantém o corpo completo, não um StreamedResponse orientado a callback. O padrão StreamedResponse desta página se aplica apenas a Laravel e Symfony.
  • Não escreva no corpo após retornar. O callback de transmissão é o dono da saída. Não faça echo nem escreva no corpo da resposta por conta própria depois de devolver o StreamedResponse ao framework.
  • Documentos assinados falham rápido. Chamar getPdfData() em um documento configurado para uma assinatura PAdES de alto nível lança NextPDF\Exception\NotImplementedException em vez de emitir um arquivo não assinado. Transmita a saída assinada pelo caminho de assinatura documentado, não por meio desta receita.

A transmissão limita a cópia da resposta, não a construção do documento. O pico de memória é aproximadamente o tamanho de um PDF finalizado, porque getPdfData() materializa o documento inteiro antes de enviar o primeiro fragmento. Para um documento genuinamente grande ou de várias páginas, a própria construção, não a transferência, domina o orçamento da requisição. Mova a geração para fora da thread da requisição com um job em fila. Consulte Gere um PDF em um job em fila.

O tamanho dos fragmentos de 64 KB é fixo e determinístico em ambas as integrações. Ele controla apenas a granularidade da transferência e não altera o total de bytes enviados nem o pico de memória. Escolha a variante transmitida quando a cópia economizada da resposta for a restrição e uma barra de progresso não for necessária. Escolha a variante em buffer para respostas pequenas e sensíveis à latência que se beneficiam de um Content-Length conhecido.

  • Valide a entrada antes de construir. A action de produção rejeita um identificador fora do intervalo com um 422 antes que qualquer trabalho de construção seja executado. Nunca interpole entrada não validada na construção ou no nome do arquivo.
  • A sanitização do nome do arquivo é aplicada para você. Ambas as factories de transmissão sanitizam o nome do arquivo e adicionam o conjunto de cabeçalhos de fortalecimento de resposta do OWASP. Passe um valor que você controla e deixe a factory sanitizá-lo como uma segunda camada. Não faça a codificação do nome do arquivo manualmente.
  • Limite a memória concorrente. Como o PDF inteiro é materializado na memória por requisição, um tráfego concorrente elevado multiplica o pico de memória. Imponha limites de tamanho e de taxa sobre as entradas que disparam uma construção para mitigar negação de serviço por esgotamento de memória.
  • Registre a classe da falha, não a mensagem. O bloco catch registra $exception::class e um identificador de correlação, nunca a mensagem da exceção ou um stack trace. Um trace bruto em um destino de log é um vazamento de informação.
  • Sem catch vazio. Todo ramo catch desta página registra e retorna uma resposta de erro definida.

Este guia não faz nenhuma afirmação normativa sobre padrões. Cada classe, método e cabeçalho mostrado faz parte da API pública verificada da integração nomeada: NextPDF\Core\Document::getPdfData(), as factories de transmissão NextPDF\Laravel\Http\PdfResponse e NextPDF\Symfony\Http\PdfResponse, e o tipo de retorno Symfony\Component\HttpFoundation\StreamedResponse. A semântica dos cabeçalhos de fortalecimento de resposta do OWASP que as factories aplicam está documentada, com suas citações, na página de segurança e operações de cada integração indicada em Consulte também. Esta página do cookbook reafirma o uso e delega as citações normativas a essas páginas.