Mengirim PDF besar yang dihasilkan sebagai respons HTTP secara streaming
Sekilas pandang
Bagian berjudul “Sekilas pandang”Anda menghasilkan PDF besar di dalam controller dan ingin mengembalikan byte-nya tanpa menyimpan salinan penuh kedua di buffer respons. Setiap integrasi framework menyediakan varian streamed dari factory PdfResponse: streamInline() dan streamDownload(). Setiap metode mengembalikan StreamedResponse dari framework dengan callback yang menulis isi PDF ke klien dalam potongan tetap 64 KB.
Pahami model memori sebelum Anda memilih jalur ini. Mesin membangun dokumen lengkap di memori terlebih dahulu. Callback streamed memanggil getPdfData(), yang menghasilkan seluruh PDF sebagai satu string, lalu menelusuri string itu dalam potongan 64 KB. Anda menghemat biaya memori puncak untuk salinan kedua yang akan disimpan oleh Illuminate\Http\Response atau Symfony\Component\HttpFoundation\Response yang ter-buffer saat framework mengukur Content-Length. Varian streamed tidak mengukur panjangnya, jadi ia menghilangkan Content-Length. Varian ini tidak pernah menyimpan isi respons dan string dokumen pada saat yang bersamaan. Ini bukan streaming inkremental yang sesungguhnya: NextPDF tidak memiliki antarmuka penulis inkremental, jadi dokumen sepenuhnya diwujudkan sebelum byte pertama mencapai socket.
Sebelum mulai, pastikan bagian-bagian berikut sudah tersedia:
- Inti NextPDF terpasang dan satu integrasi framework,
nextpdf/laravelataunextpdf/symfony, terpasang dan terdeteksi. - Anda sudah tahu cara merutekan permintaan ke sebuah controller di framework Anda.
- Anda telah membaca Kembalikan PDF yang dihasilkan dari sebuah controller, yang membahas factory ter-buffer
inline()dandownload()yang menjadi dasar resep ini.
Panduan ini berfokus pada pola StreamedResponse yang sama-sama digunakan oleh Laravel dan Symfony. CodeIgniter 4 menyertakan nama metode streamInline() / streamDownload() yang sama, tetapi membungkus byte-nya dalam CodeIgniter\HTTP\DownloadResponse, bukan StreamedResponse yang berbasis callback. Bagian Kasus tepi membahas perbedaan tersebut.
Pasang
Bagian berjudul “Pasang”Pasang integrasi untuk framework Anda. Jalankan salah satu perintah berikut.
composer require nextpdf/laravelcomposer require nextpdf/symfonyUntuk Laravel, publikasikan konfigurasinya setelah pemasangan.
php artisan vendor:publish --tag=nextpdf-configSymfony mendaftarkan bundle melalui Flex. Pastikan deteksinya berhasil pada halaman pemasangan framework Anda sebelum melanjutkan.
Tinjauan konseptual
Bagian berjudul “Tinjauan konseptual”Factory respons ter-buffer, PdfResponse::download() atau PdfResponse::inline(), memanggil getPdfData(), menyimpan string yang dikembalikan pada objek Response, dan menyetel Content-Length dari strlen(). Framework kemudian menyimpan string itu selama masa hidup respons. Untuk dokumen besar, string dokumen dan string isi respons berada di memori pada saat yang bersamaan.
Factory streamed menggunakan bentuk yang berbeda. PdfResponse::streamDownload() dan PdfResponse::streamInline() mengembalikan StreamedResponse yang dibangun dengan sebuah callback. Framework memanggil callback itu hanya ketika siap mengirimkan isi. Di dalam callback, integrasi memanggil getPdfData() sekali, memotong string yang dikembalikan menjadi potongan 64 KB, dan menjalankan echo untuk setiap potongan diikuti dengan flush(). Factory ini tidak menyimpan salinan isi kedua yang persisten, dan tidak mengirim header Content-Length.
Dua fakta membentuk setiap keputusan di halaman ini:
- Pembangunan bersifat eager, transfer dipecah menjadi potongan.
getPdfData()padaNextPDF\Core\Documentmemanggil penulis dan mengembalikan seluruh PDF sebagai satu string. Pemotongan 64 KB hanya mengontrol bagaimana byte yang sudah terbentuk meninggalkan proses. Memori puncak dibatasi oleh ukuran satu dokumen jadi, bukan oleh jendela streaming yang kecil. - Tanpa
Content-Length. Varian streamed tidak dapat mengetahui panjang isi tanpa membangunnya di dalam callback, jadi ia menghilangkan header tersebut. Bilah kemajuan klien, permintaanRange, atau proksi yang bergantung pada panjang tidak akan melihat ukurannya. Pilihdownload()/inline()yang ter-buffer ketika panjang yang diketahui lebih penting daripada menghemat salinan respons.
Dapatkan dokumen melalui jalur resolusi yang idiomatik untuk framework:
- Laravel: resolve
NextPDF\Contracts\DocumentFactoryInterfacedari container dan panggilcreate(). Metode ini mengembalikanNextPDF\Core\Documentyang baru, tipe konkret yang diterima oleh factory streamed. - Symfony: suntikkan
NextPDF\Symfony\Service\PdfFactorydan panggilcreate(). Metode ini mengembalikanNextPDF\Core\Documentyang baru dengan nilai standar terkonfigurasi yang diterapkan.
Permukaan API
Bagian berjudul “Permukaan API”| Aspek | Laravel | Symfony |
|---|---|---|
| Dokumen baru | app(DocumentFactoryInterface::class)->create() | PdfFactory::create() |
| Streamed inline | PdfResponse::streamInline($doc, $name) | PdfResponse::streamInline($doc, $name) |
| Streamed download | PdfResponse::streamDownload($doc, $name) | PdfResponse::streamDownload($doc, $name) |
| Tipe yang dikembalikan | Symfony\Component\HttpFoundation\StreamedResponse | Symfony\Component\HttpFoundation\StreamedResponse |
| Pemanggilan build dalam callback | NextPDF\Core\Document::getPdfData() | NextPDF\Core\Document::getPdfData() |
| Ukuran potongan | 64 KB (str_split deterministik) | 64 KB (loop substr deterministik) |
PdfResponse Laravel berada di NextPDF\Laravel\Http\PdfResponse; PdfResponse Symfony berada di NextPDF\Symfony\Http\PdfResponse. Kedua factory streamed mengembalikan tipe Symfony\Component\HttpFoundation\StreamedResponse yang sama. Keduanya menerapkan set header pengerasan respons Open Web Application Security Project (OWASP) yang sama dan tetap (X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Content-Security-Policy: default-src 'none', X-Robots-Tag: noindex, nofollow, Referrer-Policy: no-referrer), dan keduanya membersihkan nama berkas unduhan. Anda tidak perlu menambahkan header tersebut sendiri.
Kedua factory memanggil permukaan inti yang sama, NextPDF\Core\Document::getPdfData(): string, yang membangun dan mengembalikan seluruh biner PDF. Metode pendampingnya, save(string $path): void, menulis byte yang sama ke disk melalui penulis atomik. Resep ini menggunakan getPdfData() karena targetnya adalah socket HTTP, bukan berkas.
Contoh kode — Mulai cepat
Bagian berjudul “Contoh kode — Mulai cepat”Berikut contoh aksi unduhan streamed minimal di setiap framework. Pembuatan dokumen menggunakan permukaan inti yang sama; hanya kerangka controller-nya yang berbeda. Factory streamed menyerahkan callback ke framework, jadi aksi Anda segera kembali. Isi dibangun dan di-flush ketika framework mengirimkan respons.
<?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'); }}Untuk menampilkan pratinjau di tab browser alih-alih memaksa unduhan, panggil streamInline(...) alih-alih streamDownload(...). Content-Disposition menjadi inline, dan semua header lainnya tetap sama.
Contoh kode — Produksi
Bagian berjudul “Contoh kode — Produksi”Aksi produksi menyuntikkan dependensi, memvalidasi input path, menangkap eksepsi paling spesifik yang dapat ditimbulkan oleh proses build, mencatat kelas kegagalan tanpa membocorkan trace, dan mengembalikan kesalahan Hypertext Transfer Protocol (HTTP) yang terdefinisi. Contoh di bawah ini menggunakan injeksi konstruktor Laravel. Padanan Symfony mengikuti bentuk yang sama, dengan PdfFactory disuntikkan per aksi.
getPdfData() berjalan di dalam callback streamed, jadi eksepsi yang ditimbulkannya muncul setelah framework mulai mengirimkan header. Agar penanganan kesalahan tetap berguna, bangun dokumen (langkah yang dapat gagal) sebelum Anda menyerahkan respons kembali, lalu tangkap kegagalan build di sana. Dengan begitu, hanya transfer byte yang sudah terbentuk dalam bentuk potongan yang terjadi di dalam callback.
<?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; }}Tangkap NextPDF\Exception\NextPdfException, basis abstrak yang diwarisi oleh setiap eksepsi NextPDF, ketika Anda membutuhkan satu handler untuk setiap kegagalan build. Untuk merespons penyebab spesifik, tangkap subtipe konkret yang dapat ditimbulkan oleh getPdfData() terlebih dahulu: NextPDF\Exception\PageLayoutException ketika konten tidak dapat termuat pada geometri halaman, NextPDF\Exception\CompressionException ketika kompresi stream gagal, dan NextPDF\Exception\InvalidConfigException untuk konfigurasi output yang tidak valid. Jangan pernah menulis blok catch yang kosong. Setiap cabang di sini mencatat kelas kegagalan dan mengembalikan status yang terdefinisi.
Membuat dokumen baru per aksi melalui factory membuat factory tetap dapat ditukar dalam pengujian. Jangan menggunakan kembali satu instance controller untuk dua dokumen yang tidak berkaitan di dalam satu proses worker yang berjalan lama, karena status konten lama dapat terbawa.
Kasus tepi & jebakan
Bagian berjudul “Kasus tepi & jebakan”- Dokumen dibangun dua kali dalam pola validasi-lalu-stream. Contoh produksi memanggil
getPdfData()sekali untuk memvalidasi build, lalu factory memanggilnya lagi di dalam callback. Ini adalah biaya untuk memindahkan titik kegagalan ke sebelum header dikirim. Ketika build ganda terlalu mahal untuk dokumen tertentu, lewati probe pra-build dan terima bahwa kegagalan build di dalam callback akan memotong respons yang sudah dimulai. - Tanpa
Content-Length. Varian streamed menghilangkan header tersebut. Bilah kemajuan unduhan dan permintaanRangetidak akan berfungsi. Gunakandownload()/inline()yang ter-buffer ketika panjang yang diketahui diperlukan. - Proksi yang melakukan buffering meniadakan manfaatnya. Reverse proxy atau buffer output PHP yang menangkap seluruh isi sebelum meneruskannya akan menyimpan PDF penuh lagi, yang menghapus salinan yang dihemat. Konfigurasikan proksi untuk mengalirkan respons
application/pdf, atau gunakan respons ter-buffer pada jalur tersebut. - CodeIgniter 4 tidak melakukan streaming berbasis callback. Integrasi CodeIgniter menyertakan nama metode
streamInline()/streamDownload()yang sama, tetapi keduanya mengembalikanCodeIgniter\HTTP\DownloadResponseyang menyimpan isi penuh, bukanStreamedResponseyang berbasis callback. Pola StreamedResponse di halaman ini hanya berlaku untuk Laravel dan Symfony. - Jangan menulis ke isi setelah mengembalikan. Callback streamed memegang kendali atas output. Jangan melakukan
echoatau menulis ke isi respons sendiri setelah Anda mengembalikanStreamedResponseke framework. - Dokumen bertanda tangan gagal dengan cepat. Memanggil
getPdfData()pada dokumen yang disiapkan untuk tanda tangan PAdES tingkat tinggi menimbulkanNextPDF\Exception\NotImplementedExceptionalih-alih memancarkan berkas yang tidak ditandatangani. Alirkan output bertanda tangan melalui jalur penandatanganan yang terdokumentasi, bukan melalui resep ini.
Kinerja
Bagian berjudul “Kinerja”Streaming membatasi salinan respons, bukan proses build dokumen. Memori puncak kira-kira sebesar satu PDF jadi, karena getPdfData() mewujudkan seluruh dokumen sebelum mengirimkan potongan pertama. Untuk dokumen yang benar-benar besar atau multi-halaman, proses build itu sendiri, bukan transfer, yang mendominasi anggaran permintaan. Pindahkan pembuatan PDF keluar dari thread permintaan dengan tugas antrean. Lihat Hasilkan PDF dalam tugas antrean.
Ukuran potongan 64 KB bersifat tetap dan deterministik di kedua integrasi. Nilai ini hanya mengontrol granularitas transfer dan tidak mengubah total byte yang dikirim atau memori puncak. Pilih varian streamed ketika salinan respons yang dihemat menjadi kendala dan bilah kemajuan tidak diperlukan. Pilih varian ter-buffer untuk respons kecil yang peka terhadap latensi dan mendapat manfaat dari Content-Length yang diketahui.
Catatan keamanan
Bagian berjudul “Catatan keamanan”- Validasi input sebelum membangun. Aksi produksi menolak identifier di luar rentang dengan
422sebelum pekerjaan build apa pun berjalan. Jangan pernah menyisipkan input yang belum tervalidasi ke dalam build atau nama berkas. - Pembersihan nama berkas diterapkan untuk Anda. Kedua factory streamed membersihkan nama berkas dan menambahkan set header pengerasan respons OWASP. Berikan nilai yang Anda kendalikan dan biarkan factory membersihkannya sebagai lapisan kedua. Jangan meng-encode nama berkas secara manual.
- Batasi memori konkuren. Karena seluruh PDF diwujudkan di memori per permintaan, lalu lintas konkuren yang tinggi melipatgandakan memori puncak. Terapkan batas ukuran dan laju pada input yang menggerakkan proses build untuk mengurangi risiko serangan penolakan layanan akibat kehabisan memori.
- Catat kelas kegagalan, bukan pesannya. Blok catch mencatat
$exception::classdan identifier korelasi, tidak pernah pesan eksepsi atau stack trace. Trace mentah di dalam log sink merupakan kebocoran informasi. - Tanpa catch kosong. Setiap cabang catch di halaman ini mencatat dan mengembalikan respons kesalahan yang terdefinisi.
Kesesuaian
Bagian berjudul “Kesesuaian”Panduan ini tidak membuat klaim standar normatif apa pun. Setiap kelas, metode, dan header yang ditampilkan merupakan permukaan publik yang terverifikasi dari integrasi yang disebutkan: NextPDF\Core\Document::getPdfData(), factory streamed NextPDF\Laravel\Http\PdfResponse dan NextPDF\Symfony\Http\PdfResponse, dan tipe kembalian Symfony\Component\HttpFoundation\StreamedResponse. Semantik header pengerasan respons OWASP yang diterapkan factory tersebut didokumentasikan, beserta kutipannya, pada halaman keamanan dan operasi setiap integrasi yang ditautkan di bawah Lihat juga. Halaman cookbook ini menyatakan ulang penggunaannya dan menyerahkan kutipan normatif ke halaman-halaman tersebut.
Lihat juga
Bagian berjudul “Lihat juga”- Kembalikan PDF yang dihasilkan dari sebuah controller: padanan ter-buffer
inline()dandownload(). - Hasilkan PDF dalam tugas antrean: pindahkan proses build keluar dari thread permintaan.
- Penggunaan produksi Laravel: controller dengan DI, set header OWASP, dan kontrak pengikatan container.
- Penggunaan produksi Symfony: callback streamed, pemancar potongan 64 KB, dan locator pembangun.