Unduhan berkas dua gigabyte yang dibangun dengan file_get_contents() lalu echo menghasilkan string dua gigabyte di memori — memory_limit pecah, request gagal, dan pengguna menerima halaman error setelah menunggu lama. Pola yang benar untuk respons besar adalah streaming: mengirim data potongan demi potongan sehingga memori yang dipakai tetap kecil berapa pun besar responsnya. Pada chapter ini kita akan memakai readfile() dan fpassthru() untuk berkas, fread() untuk kontrol per potongan, generator untuk ekspor CSV baris demi baris, lalu memahami output buffering dan flush() agar data benar-benar sampai ke klien segera. Contoh di sini dijalankan di CLI agar teruji; blok yang membutuhkan web server disajikan terpisah.
readfile dan fpassthru #
Dua function built-in mengirim seluruh isi berkas ke output tanpa pernah menampungnya sebagai string PHP. readfile() membuka berkas, mendorong isinya, dan menutupnya dalam satu panggilan; fpassthru() melakukan hal yang sama dari handle yang Anda buka sendiri:
<?php
$namaFile = tempnam(sys_get_temp_dir(), "unduhan");
file_put_contents($namaFile, "baris-1\nbaris-2\nbaris-3\n");
// readfile(): buka berkas, dorong isinya ke output, tutup — satu panggilan
$byteReadfile = readfile($namaFile);
echo "\nreadfile() mengirim {$byteReadfile} byte\n";
// fpassthru(): sama, tetapi dari handle yang Anda kelola sendiri
$handle = fopen($namaFile, "rb");
$byteFpassthru = fpassthru($handle);
fclose($handle);
echo "\nfpassthru() mengirim {$byteFpassthru} byte\n";
unlink($namaFile);Output yang Dihasilkan:
baris-1
baris-2
baris-3
readfile() mengirim 24 byte
baris-1
baris-2
baris-3
fpassthru() mengirim 24 byteContoh di atas membuat berkas sementara tiga baris lewat tempnam() lalu membersihkannya dengan unlink() — di CLI, output mengalir ke terminal; di aplikasi web, output yang sama mengalir ke klien. Kedua function mengembalikan jumlah byte yang berhasil dikirim, angka yang berguna untuk mencatat log. Isi berkas tidak pernah menjadi string penampung di PHP: isi dibaca dan dikirim per potongan internal, sehingga berkas 2 GB pun dikirim dengan pemakaian memori yang nyaris konstan. Pilih readfile() untuk kasus polos; pilih fpassthru() bila Anda sudah membuka handle sendiri — misalnya setelah memeriksa header berkas atau mengunci berkas dengan flock().
Streaming Manual per Potongan #
Kadang Anda butuh kendali atas potongan yang dikirim: mengubah isi, membatasi kecepatan, atau mengukur progres. Untuk itu, loop fread() dengan ukuran potongan pilihan Anda:
<?php
$namaFile = tempnam(sys_get_temp_dir(), "unduhan");
file_put_contents($namaFile, "AAAA\nBBBB\nCCCC\n");
// Streaming manual dengan fread(): proses sepotong demi sepotong
$handle = fopen($namaFile, "rb");
$ukuranChunk = [];
while (($potongan = fread($handle, 8)) !== false) {
if ($potongan === "") {
break; // akhir berkas
}
$ukuranChunk[] = strlen($potongan);
}
fclose($handle);
echo "Berkas 15 byte diproses dalam potongan: "
. implode("-", $ukuranChunk) . " byte\n";
unlink($namaFile);Output yang Dihasilkan:
Berkas 15 byte diproses dalam potongan: 8-7 byteLoop membaca berkas 15 byte dengan potongan 8 byte sehingga menghasilkan dua potongan berukuran 8 dan 7 byte. Di dalam loop, $potongan adalah satu-satunya data yang hidup di memori — di sinilah tempat melakukan transformasi (kompresi, enkripsi per potongan) atau mengirim update progres. Ukuran potongan 8 KB hingga 1 MB adalah wilayah yang lazim: terlalu kecil berarti terlalu banyak iterasi dan syscall, terlalu besar berarti memori naik tanpa tambahan kecepatan berarti.
Ekspor CSV dengan Generator #
Ekspor data ke CSV adalah wujud paling umum dari respons besar yang dibangun bertahap: ukurannya tidak diketahui di muka dan barisnya lahir dari query database. Kombinasi generator + stream php://output menyelesaikannya dengan memori satu baris:
<?php
function barisPesanan(): Generator
{
yield ["id", "pelanggan", "total"];
yield [1, "Budi", 150000];
yield [2, "Siti", 75000];
yield [3, "Anton", 210000];
}
// Tulis baris demi baris ke stream output — tanpa array besar di memori
$out = fopen("php://output", "w");
foreach (barisPesanan() as $baris) {
fputcsv($out, $baris, separator: ",", enclosure: "\"", escape: "\\");
}
fclose($out);Output yang Dihasilkan:
id,pelanggan,total
1,Budi,150000
2,Siti,75000
3,Anton,210000Generator barisPesanan() mewakili sumber data yang datang bertahap — pada aplikasi nyata, setiap yield diisi dari $query->fetch() sehingga baris database mengalir langsung menjadi baris CSV. Stream php://output menulis langsung ke output request (di CLI berarti terminal), dan fputcsv() menangani format: pemisah, tanda kutip, dan karakter khusus. Pada kapan pun generator menghasilkan baris berikutnya, baris sebelumnya sudah tidak dibutuhkan — memori tetap selebar satu baris meskipun ekspornya jutaan baris.
Output Buffering dan flush #
Antara echo Anda dan klien terdapat lapisan-lapisan penyangga: output buffer PHP, lalu buffer web server. Fungsi-fungsi ob_* mengendalikan lapisan PHP — memahami urutannya penting agar streaming tidak terjebak di penyangga:
<?php
echo "[1] langsung ke output\n";
ob_start(); // mulai menangkap output
echo "[2] ditangkap buffer\n";
$panjang = ob_get_length(); // ukuran isi buffer saat ini
ob_end_clean(); // buang isi buffer
echo "[3] buffer tadi {$panjang} byte, dibuang tanpa tampil\n";
ob_start(); // buffering kedua
echo "[4] ditangkap lagi\n";
ob_end_flush(); // kirim isi buffer ke output
echo "[5] setelah flush, output kembali langsung\n";Output yang Dihasilkan:
[1] langsung ke output
[3] buffer tadi 21 byte, dibuang tanpa tampil
[4] ditangkap lagi
[5] setelah flush, output kembali langsungBaris [1] langsung keluar karena belum ada buffering aktif. Setelah ob_start(), baris [2] tertangkap: ob_get_length() melaporkan ukurannya, dan ob_end_clean() membuangnya tanpa pernah tampil. Buffering kedua diakhiri ob_end_flush() yang mengirim isi buffer ke output — inilah pasangan yang dipakai streaming: tulis ke buffer, kirim bila waktunya tepat. Untuk respons besar, tujuannya justru sebaliknya: memastikan tidak ada buffer yang menahan data. Pola standarnya:
<?php
// Pastikan seluruh output buffer PHP sudah dikirim sebelum flush()
while (ob_get_level() > 0) {
ob_end_flush();
}
flush(); // dorong data dari PHP ke web server
Loop while melucuti seluruh lapisan buffering yang mungkin aktif (dari framework, middleware, atau konfigurasi output_buffering), lalu flush() memaksa PHP mendorong apa yang sudah ditulis ke lapisan berikutnya. Di dalam loop streaming yang panjang, panggil flush() secara berkala setelah beberapa potongan terkirim agar klien mulai menerima data saat proses masih berjalan — bukan menunggu seluruh respons selesai dibangun.
Content-Length atau Chunked #
Ada dua cara HTTP mengirim respons besar, dan pilihan Anda menentukan pengalaman klien. Bila ukuran respons diketahui di muka — berkas di disk yang ukurannya bisa dibaca dengan filesize() — kirim header Content-Length: klien dapat menampilkan progres unduhan yang akurat dan mendeteksi unduhan yang putus di tengah jalan. Bila ukuran tidak diketahui — ekspor dari generator yang barisnya lahir bertahap — server otomatis memakai chunked transfer encoding: data dikirim sebagai rangkaian potongan bertanda panjang. Berikut pola yang pertama:
<?php
$berkas = "/data/laporan-bulanan.zip";
header("Content-Type: application/zip");
// Ukuran diketahui sejak awal → klien bisa menampilkan progres unduhan
header("Content-Length: " . filesize($berkas));
readfile($berkas);Pola header() + readfile() ini hanya berlaku di lingkungan web — header adalah konsep SAPI web dan diabaikan pada CLI. Satu peringatan penting: Content-Length harus akurat. Bila sesuatu (spasi kosong sebelum <?php, notice yang tercetak) menambah satu byte pun sebelum berkas, unduhan rusak karena panjangnya tidak lagi cocok — alasan lain memastikan output buffer bersih sebelum mengirim berkas.
Header untuk Produksi dan X-Accel-Buffering #
Endpoint ekspor di aplikasi web nyata menggabungkan seluruh bahan chapter ini: header yang benar, generator sebagai sumber baris, dan php://output sebagai tujuan:
<?php
// Endpoint unduhan CSV di aplikasi web nyata
header("Content-Type: text/csv; charset=utf-8");
header("Content-Disposition: attachment; filename=pesanan.csv");
header("X-Accel-Buffering: no"); // matikan buffering Nginx agar data langsung mengalir
$out = fopen("php://output", "w");
foreach (barisPesanan() as $baris) {
fputcsv($out, $baris, separator: ",", enclosure: "\"", escape: "\\");
}
fclose($out);Content-Disposition: attachment meminta browser menyimpan hasil sebagai berkas pesanan.csv alih-alih menampilkannya. Header X-Accel-Buffering: no penting di belakang Nginx: secara default Nginx menyangga respons FastCGI sebelum meneruskannya (bagus untuk performa halaman biasa, buruk untuk streaming karena seluruh respons ditumpuk dulu). Header khusus ini mematikan buffering per respons sehingga tiap flush() benar-benar sampai ke klien — mekanisme yang sama dipakai server-sent events agar event terkirim seketika.
Ringkasan #
Respons besar tidak membutuhkan memori besar: readfile() dan fpassthru() mengirim berkas tanpa penampung string, loop fread() memberi kendali per potongan, generator mengalirkan baris database menjadi baris CSV melalui php://output, dan pemahaman ob_* serta flush() memastikan data benar-benar tiba di klien segera. Content-Length untuk ukuran yang diketahui, chunked untuk yang bertahap, ditambah X-Accel-Buffering: no di belakang Nginx melengkapi perlengkapan produksinya. Semua optimasi section ini — cache, query, memori, streaming — kini menuntut satu hal terakhir: bukti terukur bahwa aplikasi memang cepat, dan itu pekerjaan chapter penutup, Performance Testing
.