Een grote gegenereerde PDF streamen als HTTP-respons
In het kort
Sectie met titel “In het kort”U genereert een grote PDF in een controller en wilt de bytes retourneren zonder een tweede volledige kopie in de responsbuffer te bewaren. Elke frameworkintegratie bevat gestreamde varianten van de bijbehorende PdfResponse-factory: streamInline() en streamDownload(). Beide methoden retourneren een StreamedResponse van het framework met een callback die de PDF-body in vaste chunks van 64 KB naar de client schrijft.
Lees het geheugenmodel voordat u voor dit pad kiest. De engine bouwt eerst het volledige document op in het geheugen. De gestreamde callback roept getPdfData() aan; die methode materialiseert de hele PDF als één string en doorloopt die string vervolgens in stukken van 64 KB. U bespaart de piekgeheugenkosten van de tweede kopie die een gebufferde Illuminate\Http\Response of Symfony\Component\HttpFoundation\Response zou vasthouden terwijl het framework Content-Length meet. De gestreamde variant meet de lengte niet en laat daarom Content-Length weg. Daardoor worden de responsbody en de documentstring nooit tegelijkertijd vastgehouden. Dit is geen echte incrementele streaming: NextPDF heeft geen incrementele writer-interface, dus het document wordt volledig gerealiseerd voordat de eerste byte de socket bereikt.
Controleer voordat u begint of deze onderdelen aanwezig zijn:
- NextPDF-core en één frameworkintegratie,
nextpdf/laravelofnextpdf/symfony, zijn geïnstalleerd en gedetecteerd. - U weet hoe u een verzoek naar een controller in uw framework routeert.
- U hebt Een gegenereerde PDF retourneren vanuit een controller gelezen; die pagina behandelt de gebufferde
inline()- endownload()-factory waarop dit recipe voortbouwt.
Deze how-to richt zich op het StreamedResponse-patroon dat Laravel en Symfony delen. CodeIgniter 4 levert dezelfde methodenamen streamInline() / streamDownload(), maar verpakt de bytes in een CodeIgniter\HTTP\DownloadResponse in plaats van een callback-gedreven StreamedResponse. De sectie Randgevallen behandelt dat verschil.
Installeren
Sectie met titel “Installeren”Installeer de integratie voor uw framework. Voer een van de volgende commando’s uit.
composer require nextpdf/laravelcomposer require nextpdf/symfonyPubliceer voor Laravel de configuratie na de installatie.
php artisan vendor:publish --tag=nextpdf-configSymfony registreert de bundle via Flex. Controleer de detectie op de installatiepagina van uw framework voordat u verdergaat.
Conceptueel overzicht
Sectie met titel “Conceptueel overzicht”Een gebufferde responsfactory, PdfResponse::download() of PdfResponse::inline(), roept getPdfData() aan, slaat de geretourneerde string op in een Response-object en stelt Content-Length in op basis van strlen(). Het framework houdt die string vervolgens gedurende de levensduur van de respons vast. Bij een groot document staan de documentstring en de responsbody-string tegelijkertijd in het geheugen.
De gestreamde factory gebruikt een andere vorm. PdfResponse::streamDownload() en PdfResponse::streamInline() retourneren een StreamedResponse die met een callback is opgebouwd. Het framework roept die callback pas aan wanneer het klaar is om de body te verzenden. Binnen de callback roept de integratie getPdfData() eenmaal aan, splitst de geretourneerde string in chunks van 64 KB en schrijft elke chunk met echo weg, gevolgd door een flush(). De factory bewaart geen tweede blijvende kopie van de body en geeft geen Content-Length-header mee.
Twee feiten zijn bepalend voor elke beslissing op deze pagina:
- De build is eager, de overdracht is chunked.
getPdfData()opNextPDF\Core\Documentroept de writer aan en retourneert de hele PDF als één string. De chunking van 64 KB bepaalt alleen hoe de al gebouwde bytes het proces verlaten. Het piekgeheugen wordt begrensd door de omvang van één voltooid document, niet door een klein streamingvenster. - Geen
Content-Length. De gestreamde variant kan de body-lengte niet kennen zonder die binnen de callback op te bouwen en laat de header daarom weg. Een voortgangsbalk aan clientzijde, eenRange-verzoek of een lengtegevoelige proxy ziet geen omvang. Kies de gebufferdedownload()/inline()wanneer een bekende lengte belangrijker is dan het besparen van de responskopie.
Haal het document op via het idiomatische resolutiepad van het framework:
- Laravel: resolve
NextPDF\Contracts\DocumentFactoryInterfacevanuit de container en roepcreate()aan. Dit retourneert een verseNextPDF\Core\Document, het concrete type dat de gestreamde factory’s accepteren. - Symfony: injecteer
NextPDF\Symfony\Service\PdfFactoryen roepcreate()aan. Het retourneert een verseNextPDF\Core\Documentmet de geconfigureerde standaardwaarden toegepast.
API-oppervlak
Sectie met titel “API-oppervlak”| Aandachtspunt | Laravel | Symfony |
|---|---|---|
| Vers document | app(DocumentFactoryInterface::class)->create() | PdfFactory::create() |
| Gestreamd inline | PdfResponse::streamInline($doc, $name) | PdfResponse::streamInline($doc, $name) |
| Gestreamde download | PdfResponse::streamDownload($doc, $name) | PdfResponse::streamDownload($doc, $name) |
| Geretourneerd type | Symfony\Component\HttpFoundation\StreamedResponse | Symfony\Component\HttpFoundation\StreamedResponse |
| Build-aanroep binnen de callback | NextPDF\Core\Document::getPdfData() | NextPDF\Core\Document::getPdfData() |
| Chunkgrootte | 64 KB (deterministische str_split) | 64 KB (deterministische substr-lus) |
De Laravel-PdfResponse bevindt zich in NextPDF\Laravel\Http\PdfResponse; de Symfony-variant bevindt zich in NextPDF\Symfony\Http\PdfResponse. Hun gestreamde factory’s retourneren beide hetzelfde type Symfony\Component\HttpFoundation\StreamedResponse. Beide passen dezelfde vaste set hardening-headers van het Open Web Application Security Project (OWASP) toe (X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Content-Security-Policy: default-src 'none', X-Robots-Tag: noindex, nofollow, Referrer-Policy: no-referrer), en beide saneren de bestandsnaam voor downloads. U voegt die headers niet zelf toe.
Beide factory’s roepen hetzelfde onderliggende core-API-oppervlak aan, NextPDF\Core\Document::getPdfData(): string, dat de hele PDF-binary bouwt en retourneert. Het verwante save(string $path): void schrijft dezelfde bytes naar schijf via een atomaire writer. Dit recipe gebruikt getPdfData() omdat het doel een HTTP-socket is, niet een bestand.
Codevoorbeeld — Snelstart
Sectie met titel “Codevoorbeeld — Snelstart”Hier ziet u de minimale gestreamde downloadactie in elk framework. De documentaanroepen gebruiken hetzelfde core-oppervlak; alleen de controllerstructuur verschilt. De gestreamde factory geeft het framework een callback, zodat uw actie onmiddellijk terugkeert. De body wordt gebouwd en geflusht wanneer het framework de respons verzendt.
<?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'); }}<?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'); }}Roep streamInline(...) aan in plaats van streamDownload(...) om de PDF in een browsertabblad te tonen in plaats van een download af te dwingen. De Content-Disposition wordt inline en elke andere header blijft gelijk.
Codevoorbeeld — Productie
Sectie met titel “Codevoorbeeld — Productie”Een productieactie injecteert haar afhankelijkheden, valideert de invoer uit het pad, vangt de meest specifieke uitzondering op die de build kan opwerpen, logt de foutklasse zonder een trace te lekken en retourneert een gedefinieerde Hypertext Transfer Protocol (HTTP)-foutrespons. Het onderstaande voorbeeld gebruikt Laravel-constructorinjectie. De Symfony-tegenhanger volgt dezelfde vorm, met PdfFactory per actie geïnjecteerd.
getPdfData() draait binnen de gestreamde callback, dus een uitzondering die het opwerpt, treedt op nadat het framework al begonnen is met het verzenden van headers. Bouw het document (de stap die kan mislukken) voordat u de respons teruggeeft en vang de buildfout daar op, zodat de foutafhandeling bruikbaar blijft. Daarna vindt binnen de callback alleen nog de chunked overdracht van al gebouwde bytes plaats.
<?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; }}Vang NextPDF\Exception\NextPdfException op wanneer u één handler wilt voor elke buildfout; dit is de abstracte basis die elke NextPDF-uitzondering uitbreidt. Wilt u op specifieke oorzaken reageren, vang dan eerst de concrete subtypes op die getPdfData() kan opwerpen: NextPDF\Exception\PageLayoutException wanneer inhoud niet in de paginageometrie past, NextPDF\Exception\CompressionException wanneer streamcompressie mislukt, en NextPDF\Exception\InvalidConfigException voor een ongeldige uitvoerconfiguratie. Schrijf nooit een leeg catch-blok. Elke tak hier logt de foutklasse en retourneert een gedefinieerde status.
Door per actie een vers document te resolven, blijft de factory verwisselbaar in tests. Hergebruik niet één controller-instantie voor twee niet-gerelateerde documenten binnen één langlopend workerproces, omdat oude inhoudsstaat kan blijven hangen.
Randgevallen en valkuilen
Sectie met titel “Randgevallen en valkuilen”- Het document wordt tweemaal gebouwd in het valideer-dan-stream-patroon. Het productievoorbeeld roept
getPdfData()eenmaal aan om de build te valideren, en daarna roept de factory het opnieuw aan binnen de callback. Dit is de prijs van het verplaatsen van het faalpunt vóór de headers. Wanneer een dubbele build te duur is voor een bepaald document, sla dan de pre-build-probe over en accepteer dat een buildfout binnen de callback een respons afkapt die al is gestart. - Geen
Content-Length. De gestreamde variant laat de header weg. Voortgangsbalken voor downloads enRange-verzoeken werken niet. Gebruik de gebufferdedownload()/inline()wanneer een bekende lengte vereist is. - Een bufferende proxy doet het voordeel teniet. Een reverse proxy of PHP-outputbuffer die de hele body vastlegt voordat deze wordt doorgestuurd, houdt de volledige PDF opnieuw vast, wat de bespaarde kopie tenietdoet. Configureer de proxy om
application/pdf-responsen te streamen, of gebruik een gebufferde respons op dat pad. - CodeIgniter 4 is niet callback-gestreamd. De CodeIgniter-integratie levert dezelfde methodenamen
streamInline()/streamDownload(), maar ze retourneren eenCodeIgniter\HTTP\DownloadResponsedie de volledige body vasthoudt, geen callback-gedrevenStreamedResponse. Het StreamedResponse-patroon op deze pagina geldt alleen voor Laravel en Symfony. - Schrijf niet naar de body na het retourneren. De gestreamde callback bezit de uitvoer. Gebruik geen
echoen schrijf niet zelf naar de responsbody nadat u deStreamedResponseaan het framework hebt teruggegeven. - Ondertekende documenten falen snel. Het aanroepen van
getPdfData()op een document dat is ingesteld voor een high-level PAdES-handtekening werptNextPDF\Exception\NotImplementedExceptionop in plaats van een niet-ondertekend bestand af te geven. Stream ondertekende uitvoer via het gedocumenteerde ondertekeningspad, niet via dit recipe.
Prestaties
Sectie met titel “Prestaties”Streaming begrenst de responskopie, niet de documentbuild. Het piekgeheugen is ongeveer de omvang van één voltooide PDF, omdat getPdfData() het hele document realiseert voordat het de eerste chunk verzendt. Voor een werkelijk groot document of een document met veel pagina’s domineert de build zelf het verzoekbudget, niet de overdracht. Verplaats de generatie van de verzoekthread naar een taak in de wachtrij. Zie Een PDF genereren in een gewachtrijde taak.
De chunkgrootte van 64 KB is vast en deterministisch in beide integraties. Die bepaalt alleen de overdrachtsgranulariteit en verandert niet het totale aantal verzonden bytes of het piekgeheugen. Kies de gestreamde variant wanneer de bespaarde responskopie de beperking is en een voortgangsbalk niet vereist is. Kies de gebufferde variant voor kleine, latentiegevoelige responsen die baat hebben bij een bekende Content-Length.
Beveiligingsnotities
Sectie met titel “Beveiligingsnotities”- Valideer invoer voordat u bouwt. De productieactie wijst een identifier buiten het toegestane bereik af met een
422voordat er buildwerk start. Interpoleer nooit ongevalideerde invoer in de build of de bestandsnaam. - Bestandsnaamsanitering wordt voor u toegepast. Beide gestreamde factory’s saneren de bestandsnaam en voegen de OWASP-set hardening-headers toe. Geef een waarde door die u beheert en laat de factory deze als tweede laag saneren. Codeer de bestandsnaam niet zelf met de hand.
- Beperk het gelijktijdige geheugengebruik. Omdat de hele PDF per verzoek in het geheugen wordt gematerialiseerd, vermenigvuldigt hoge gelijktijdige belasting het piekgeheugen. Handhaaf grootte- en snelheidslimieten op de invoer die een build aanstuurt om denial of service door geheugenuitputting te beperken.
- Log de foutklasse, niet het bericht. Het catch-blok logt
$exception::classen een correlatie-identifier, nooit het uitzonderingsbericht of een stacktrace. Een ruwe trace in een log sink is een informatielek. - Geen leeg catch. Elke catch-tak op deze pagina logt en retourneert een gedefinieerde foutrespons.
Conformiteit
Sectie met titel “Conformiteit”Deze gids doet geen normatieve claim over standaarden. Elke getoonde klasse, methode en header is het geverifieerde publieke API-oppervlak van de genoemde integratie: NextPDF\Core\Document::getPdfData(), de gestreamde factory’s NextPDF\Laravel\Http\PdfResponse en NextPDF\Symfony\Http\PdfResponse, en het geretourneerde type Symfony\Component\HttpFoundation\StreamedResponse. De OWASP-hardening-header-semantiek die de factory’s toepassen, is met de bijbehorende citaties gedocumenteerd op de security-en-operations-pagina van elke integratie, gelinkt onder Zie ook. Deze cookbook-pagina herhaalt het gebruik en verwijst voor de normatieve citaties naar die pagina’s.
Zie ook
Sectie met titel “Zie ook”- Een gegenereerde PDF retourneren vanuit een controller: de gebufferde tegenhangers
inline()endownload(). - Een PDF genereren in een gewachtrijde taak: verplaats de build uit de verzoekthread.
- Laravel-productiegebruik: controller met DI-wiring, de OWASP-headerset en het container-bindingcontract.
- Symfony-productiegebruik: de gestreamde callback, de 64 KB-chunk-emitter en de builder-locator.