Strumieniowe przesyłanie dużego wygenerowanego PDF jako odpowiedź HTTP
W skrócie
Dział zatytułowany „W skrócie”Generujesz duży PDF w kontrolerze i chcesz zwrócić bajty bez trzymania drugiej pełnej kopii w buforze odpowiedzi. Każda integracja z frameworkiem udostępnia strumieniowe warianty swojej fabryki PdfResponse: streamInline() oraz streamDownload(). Każda z tych metod zwraca frameworkowy obiekt StreamedResponse z wywołaniem zwrotnym, które wysyła treść PDF do klienta w stałych fragmentach po 64 KB.
Zanim wybierzesz tę ścieżkę, poznaj model pamięci. Silnik najpierw buduje kompletny dokument w pamięci. Strumieniowe wywołanie zwrotne wywołuje getPdfData(), przez co materializuje cały PDF jako jeden łańcuch znaków, a następnie iteruje po nim w wycinkach po 64 KB. Unikasz szczytowego kosztu drugiej kopii, którą buforowana odpowiedź Illuminate\Http\Response lub Symfony\Component\HttpFoundation\Response przechowywałaby, gdy framework oblicza Content-Length. Wariant strumieniowy nie oblicza długości, więc pomija Content-Length. Nie przechowuje jednocześnie treści odpowiedzi i łańcucha dokumentu. To nie jest prawdziwe strumieniowanie przyrostowe: NextPDF nie udostępnia interfejsu zapisu przyrostowego, więc dokument jest w pełni utworzony, zanim pierwszy bajt dotrze do gniazda.
Zanim zaczniesz, upewnij się, że te elementy są na swoim miejscu:
- Rdzeń NextPDF jest zainstalowany, a jedna integracja z frameworkiem,
nextpdf/laravellubnextpdf/symfony, jest zainstalowana i wykryta. - Wiesz już, jak skierować żądanie do kontrolera w używanym frameworku.
- Znasz już stronę Zwracanie wygenerowanego PDF z kontrolera, gdzie opisano buforowane fabryki
inline()orazdownload(), na których opiera się ten przepis.
Ten przewodnik koncentruje się na wzorcu StreamedResponse wspólnym dla Laravel i Symfony. CodeIgniter 4 udostępnia metody o tych samych nazwach streamInline() / streamDownload(), ale opakowuje bajty w CodeIgniter\HTTP\DownloadResponse zamiast w sterowaną wywołaniem zwrotnym odpowiedź StreamedResponse. Tę różnicę omawia sekcja Przypadki brzegowe.
Instalacja
Dział zatytułowany „Instalacja”Zainstaluj integrację dla używanego frameworka. Uruchom jedno z poniższych poleceń.
composer require nextpdf/laravelcomposer require nextpdf/symfonyW przypadku Laravel po instalacji opublikuj konfigurację.
php artisan vendor:publish --tag=nextpdf-configSymfony rejestruje pakiet przez Flex. Przed kontynuowaniem sprawdź na stronie instalacji używanego frameworka, czy pakiet został wykryty.
Przegląd koncepcyjny
Dział zatytułowany „Przegląd koncepcyjny”Buforowana fabryka odpowiedzi, PdfResponse::download() lub PdfResponse::inline(), wywołuje getPdfData(), zapisuje zwrócony łańcuch znaków w obiekcie Response i ustawia Content-Length na podstawie strlen(). Następnie framework przechowuje ten łańcuch przez cały czas życia odpowiedzi. W przypadku dużego dokumentu łańcuch dokumentu i łańcuch treści odpowiedzi znajdują się w pamięci jednocześnie.
Strumieniowa fabryka działa inaczej. PdfResponse::streamDownload() oraz PdfResponse::streamInline() zwracają obiekt StreamedResponse zbudowany z wywołaniem zwrotnym. To wywołanie zwrotne framework uruchamia dopiero wtedy, gdy jest gotowy wysłać treść. Wewnątrz wywołania zwrotnego integracja raz wywołuje getPdfData(), dzieli zwrócony łańcuch znaków na fragmenty po 64 KB i wykonuje echo dla każdego fragmentu, a potem flush(). Nie zachowuje drugiej trwałej kopii treści ani nie emituje nagłówka Content-Length.
Wszystkie decyzje na tej stronie wynikają z dwóch faktów:
- Budowanie jest zachłanne, transfer jest fragmentowany.
getPdfData()wNextPDF\Core\Documentwywołuje moduł zapisu i zwraca cały PDF jako jeden łańcuch znaków. Fragmentowanie po 64 KB kontroluje wyłącznie sposób, w jaki już zbudowane bajty opuszczają proces. Szczytowe zużycie pamięci jest ograniczone rozmiarem jednego ukończonego dokumentu, a nie małym oknem strumieniowania. - Brak
Content-Length. Wariant strumieniowy nie może poznać długości treści bez zbudowania jej wewnątrz wywołania zwrotnego, więc pomija ten nagłówek. Pasek postępu po stronie klienta, żądanieRangeani serwer proxy wrażliwy na długość nie dostaną informacji o rozmiarze. Wybierz buforowanedownload()/inline(), gdy znana długość jest ważniejsza niż oszczędzenie kopii odpowiedzi.
Pobierz dokument przez idiomatyczny dla frameworka mechanizm rozwiązywania zależności:
- Laravel: rozwiąż
NextPDF\Contracts\DocumentFactoryInterfacez kontenera i wywołajcreate(). Zwraca świeży obiektNextPDF\Core\Document, konkretny typ akceptowany przez strumieniowe fabryki. - Symfony: wstrzyknij
NextPDF\Symfony\Service\PdfFactoryi wywołajcreate(). Zwraca świeży obiektNextPDF\Core\Documentpo zastosowaniu skonfigurowanych wartości domyślnych.
Powierzchnia API
Dział zatytułowany „Powierzchnia API”| Zagadnienie | Laravel | Symfony |
|---|---|---|
| Świeży dokument | app(DocumentFactoryInterface::class)->create() | PdfFactory::create() |
| Strumieniowo, podgląd | PdfResponse::streamInline($doc, $name) | PdfResponse::streamInline($doc, $name) |
| Strumieniowo, pobieranie | PdfResponse::streamDownload($doc, $name) | PdfResponse::streamDownload($doc, $name) |
| Zwracany typ | Symfony\Component\HttpFoundation\StreamedResponse | Symfony\Component\HttpFoundation\StreamedResponse |
| Wywołanie budujące wewnątrz wywołania zwrotnego | NextPDF\Core\Document::getPdfData() | NextPDF\Core\Document::getPdfData() |
| Rozmiar fragmentu | 64 KB (deterministyczny str_split) | 64 KB (deterministyczna pętla substr) |
Klasa PdfResponse dla Laravel znajduje się w NextPDF\Laravel\Http\PdfResponse; ta dla Symfony znajduje się w NextPDF\Symfony\Http\PdfResponse. Ich strumieniowe fabryki zwracają ten sam typ Symfony\Component\HttpFoundation\StreamedResponse. Obie stosują ten sam stały zestaw nagłówków wzmacniających odpowiedź według 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), a także oczyszczają nazwę pobieranego pliku. Nie musisz dodawać tych nagłówków samodzielnie.
Obie fabryki wywołują tę samą bazową powierzchnię rdzenia, NextPDF\Core\Document::getPdfData(): string, która buduje i zwraca cały plik binarny PDF. Jej odpowiednik save(string $path): void zapisuje te same bajty na dysk za pośrednictwem atomowego modułu zapisu. Ten przepis używa getPdfData(), ponieważ celem jest gniazdo HTTP, a nie plik na dysku.
Przykład kodu — Szybki start
Dział zatytułowany „Przykład kodu — Szybki start”Poniżej znajduje się minimalna strumieniowa akcja pobierania w każdym frameworku. Wywołania dokumentu używają tej samej powierzchni rdzenia; różni się jedynie struktura kontrolera. Strumieniowa fabryka przekazuje frameworkowi wywołanie zwrotne, więc akcja zwraca wynik natychmiast. Treść jest budowana i opróżniana, gdy framework wysyła odpowiedź.
<?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'); }}Aby wyświetlić podgląd w karcie przeglądarki zamiast wymuszać pobieranie, wywołaj streamInline(...) zamiast streamDownload(...). Nagłówek Content-Disposition przyjmuje wartość inline, a każdy inny nagłówek pozostaje taki sam.
Przykład kodu — Produkcja
Dział zatytułowany „Przykład kodu — Produkcja”Produkcyjna akcja wstrzykuje zależności, weryfikuje dane wejściowe ze ścieżki, przechwytuje najbardziej szczegółowy wyjątek, jaki może zgłosić budowanie, rejestruje klasę błędu bez ujawniania śladu stosu i zwraca zdefiniowany błąd protokołu Hypertext Transfer Protocol (HTTP). Poniższy przykład wykorzystuje wstrzykiwanie przez konstruktor w Laravel. Odpowiednik w Symfony ma tę samą postać, z PdfFactory wstrzykiwanym do akcji.
getPdfData() działa wewnątrz strumieniowego wywołania zwrotnego, więc wyjątek zgłoszony przez tę metodę ujawnia się po rozpoczęciu wysyłania nagłówków przez framework. Aby obsługa błędów pozostała użyteczna, zbuduj dokument (krok, który może się nie powieść) przed zwróceniem odpowiedzi i tam przechwyć niepowodzenie budowania. Wtedy wewnątrz wywołania zwrotnego odbywa się jedynie fragmentowany transfer już zbudowanych bajtów.
<?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; }}Przechwyć NextPDF\Exception\NextPdfException, abstrakcyjną klasę bazową, którą rozszerza każdy wyjątek NextPDF, jeśli potrzebujesz jednej procedury obsługi dowolnego niepowodzenia budowania. Aby reagować na konkretne przyczyny, najpierw przechwyć konkretne podtypy, które może zgłosić getPdfData(): NextPDF\Exception\PageLayoutException, gdy treść nie mieści się w geometrii strony, NextPDF\Exception\CompressionException, gdy kompresja strumienia się nie powiedzie, oraz NextPDF\Exception\InvalidConfigException w przypadku nieprawidłowej konfiguracji wyjścia. Nigdy nie pisz pustego bloku catch. Każda przedstawiona tu gałąź rejestruje klasę błędu i zwraca zdefiniowany status.
Tworzenie świeżego dokumentu dla każdej akcji sprawia, że fabryka pozostaje wymienialna w testach. Nie używaj ponownie jednej instancji kontrolera dla dwóch niepowiązanych dokumentów w obrębie jednego długo działającego procesu roboczego, ponieważ nieaktualny stan treści może zostać przeniesiony dalej.
Przypadki brzegowe i pułapki
Dział zatytułowany „Przypadki brzegowe i pułapki”- Dokument jest budowany dwa razy we wzorcu „weryfikuj, potem strumieniuj”. Przykład produkcyjny wywołuje
getPdfData()raz, aby sprawdzić, czy budowanie się powiedzie, a następnie fabryka wywołuje je ponownie wewnątrz wywołania zwrotnego. To koszt przesunięcia punktu niepowodzenia przed nagłówki. Gdy podwójne budowanie jest zbyt kosztowne dla danego dokumentu, pomiń wstępną próbę budowania i zaakceptuj, że niepowodzenie budowania wewnątrz wywołania zwrotnego obetnie już rozpoczętą odpowiedź. - Brak
Content-Length. Wariant strumieniowy pomija ten nagłówek. Paski postępu pobierania i żądaniaRangenie będą działać. Użyj buforowanychdownload()/inline(), gdy wymagana jest znana długość. - Buforujący serwer proxy niweczy tę korzyść. Odwrotny serwer proxy lub bufor wyjściowy PHP, który przechwytuje całą treść przed jej przekazaniem, ponownie przechowuje cały PDF, co niweczy zaoszczędzoną kopię. Skonfiguruj serwer proxy tak, aby strumieniował odpowiedzi
application/pdf, albo użyj buforowanej odpowiedzi na tej ścieżce. - CodeIgniter 4 nie używa strumieniowania z wywołaniem zwrotnym. Integracja CodeIgniter udostępnia metody o tych samych nazwach
streamInline()/streamDownload(), ale zwracają one obiektCodeIgniter\HTTP\DownloadResponse, który przechowuje całą treść, a nie sterowaną wywołaniem zwrotnym odpowiedźStreamedResponse. Wzorzec StreamedResponse na tej stronie dotyczy wyłącznie Laravel i Symfony. - Nie zapisuj do treści po zwróceniu. Strumieniowe wywołanie zwrotne odpowiada za wyjście. Nie wykonuj
echoani nie zapisuj samodzielnie do treści odpowiedzi po zwróceniuStreamedResponsedo frameworka. - Podpisane dokumenty kończą się szybkim niepowodzeniem. Wywołanie
getPdfData()na dokumencie skonfigurowanym do wysokopoziomowego podpisu PAdES zgłaszaNextPDF\Exception\NotImplementedExceptionzamiast emitować niepodpisany plik. Strumieniuj podpisane wyjście za pośrednictwem udokumentowanej ścieżki podpisywania, a nie przez ten przepis.
Wydajność
Dział zatytułowany „Wydajność”Strumieniowanie ogranicza dodatkową kopię odpowiedzi, a nie budowanie dokumentu. Szczytowe zużycie pamięci to mniej więcej rozmiar jednego ukończonego PDF, ponieważ getPdfData() materializuje cały dokument przed wysłaniem pierwszego fragmentu. W przypadku naprawdę dużego lub wielostronicowego dokumentu samo budowanie, a nie transfer, dominuje w budżecie żądania. Przenieś generowanie poza wątek żądania za pomocą zadania w kolejce. Zobacz Generowanie PDF w zadaniu w kolejce.
Rozmiar fragmentu 64 KB jest stały i deterministyczny w obu integracjach. Kontroluje wyłącznie ziarnistość transferu i nie zmienia łącznej liczby wysłanych bajtów ani szczytowego zużycia pamięci. Wybierz wariant strumieniowy, gdy ograniczeniem jest zaoszczędzona kopia odpowiedzi, a pasek postępu nie jest wymagany. Wybierz wariant buforowany dla małych, wrażliwych na opóźnienia odpowiedzi, które korzystają ze znanego Content-Length.
Uwagi dotyczące bezpieczeństwa
Dział zatytułowany „Uwagi dotyczące bezpieczeństwa”- Weryfikuj dane wejściowe przed budowaniem. Produkcyjna akcja odrzuca identyfikator spoza zakresu z kodem
422, zanim uruchomi się jakakolwiek praca związana z budowaniem. Nigdy nie wstawiaj niezweryfikowanych danych wejściowych do budowania ani do nazwy pliku. - Oczyszczanie nazwy pliku jest stosowane za Ciebie. Obie strumieniowe fabryki oczyszczają nazwę pliku i dodają zestaw nagłówków wzmacniających odpowiedź według OWASP. Przekaż wartość, którą kontrolujesz, i pozwól fabryce oczyścić ją w drugiej warstwie. Nie koduj nazwy pliku samodzielnie.
- Ogranicz współbieżne zużycie pamięci. Ponieważ cały PDF jest materializowany w pamięci na każde żądanie, wysoki ruch współbieżny zwielokrotnia szczytowe zużycie pamięci. Wymuszaj limity rozmiaru i częstotliwości dla danych wejściowych sterujących budowaniem, aby ograniczyć atak typu odmowy usługi przez wyczerpanie pamięci.
- Rejestruj klasę błędu, a nie komunikat. Blok catch rejestruje
$exception::classoraz identyfikator korelacji, nigdy komunikat wyjątku ani ślad stosu. Surowy ślad stosu w odbiorniku dzienników oznacza wyciek informacji. - Brak pustego catch. Każda gałąź catch na tej stronie rejestruje i zwraca zdefiniowaną odpowiedź błędu.
Zgodność
Dział zatytułowany „Zgodność”Ten przewodnik nie formułuje żadnego normatywnego twierdzenia dotyczącego standardów. Każda pokazana klasa, metoda i nagłówek należy do zweryfikowanej publicznej powierzchni wskazanej integracji: NextPDF\Core\Document::getPdfData(), strumieniowe fabryki NextPDF\Laravel\Http\PdfResponse i NextPDF\Symfony\Http\PdfResponse oraz zwracany typ Symfony\Component\HttpFoundation\StreamedResponse. Semantyka nagłówków wzmacniających odpowiedź według OWASP, stosowanych przez fabryki, jest udokumentowana wraz z cytowaniami na stronie bezpieczeństwa i operacji każdej integracji, powiązanej w sekcji Zobacz też. Ta strona książki kucharskiej opisuje zastosowanie i odsyła normatywne cytowania do tamtych stron.
Zobacz też
Dział zatytułowany „Zobacz też”- Zwracanie wygenerowanego PDF z kontrolera: buforowane odpowiedniki
inline()idownload(). - Generowanie PDF w zadaniu w kolejce: przenieś budowanie poza wątek żądania.
- Użycie produkcyjne w Laravel: kontroler podłączony przez DI, zestaw nagłówków OWASP oraz powiązanie kontraktu w kontenerze.
- Użycie produkcyjne w Symfony: strumieniowe wywołanie zwrotne, emiter fragmentów po 64 KB oraz lokalizator konstruktora.