Membaca berkas berukuran 2 GB dengan file_get_contents() berarti memuat 2 GB ke memori sekaligus — sering melewati memory_limit, kadang membuat proses terbunuh server, dan selalu menyia-nyiakan RAM untuk data yang sebenarnya cukup diproses sedikit demi sedikit. Solusinya adalah streaming: membuka berkas sekali, lalu membaca potongan kecil — satu baris atau satu rekaman CSV — memprosesnya, dan membuangnya sebelum potongan berikutnya dibaca. PHP menyediakan tiga lapis API untuk pola ini: fungsi fgets() dan fgetcsv(), objek SplFileObject, serta generator sebagai pembungkus yang rapi. Chapter ini mempraktikkan ketiganya, lalu menutup dengan pola memecah keluaran dan menjaga memori antar batch. Untuk mengirim berkas besar sebagai respons HTTP, polanya berbeda dan dibahas pada Chapter Streaming Large Responses di Section Caching dan Performance.

Membaca Baris demi Baris dengan Generator #

fgets() membaca satu baris dari berkas yang dibuka fopen() dan menggeser pointer posisi ke baris berikutnya — memori yang terpakai hanya sebesar satu baris, berapa pun ukuran berkasnya. Membungkus pemanggilannya ke dalam generator membuat kode pemanggil tetap bersih: pemanggil melakukan foreach biasa, sementara detail membuka, menutup, dan menggeser pointer bersembunyi di dalam fungsi:

php
<?php
$berkas = "/tmp/stream-baris.txt";
file_put_contents($berkas, "kopi arabika\nteh hijau\ngula aren\n");

function bacaBaris(string $berkas): Generator
{
    $handle = fopen($berkas, "rb");
    if ($handle === false) {
        throw new RuntimeException("Gagal membuka berkas: $berkas");
    }
    try {
        while (($baris = fgets($handle)) !== false) {
            yield rtrim($baris, "\r\n");
        }
    } finally {
        fclose($handle);
    }
}

$jumlahBaris = 0;
foreach (bacaBaris($berkas) as $nomor => $nama) {
    $jumlahBaris++;
    printf("Baris %d: %s\n", $nomor + 1, $nama);
}
echo "Total baris: $jumlahBaris\n";
unlink($berkas);

Output yang Dihasilkan:

text
Baris 1: kopi arabika
Baris 2: teh hijau
Baris 3: gula aren
Total baris: 3

Mari kita bedah bagian-bagian pentingnya:

  1. Mode "rb": berkas dibuka untuk dibaca dalam mode biner — aman di semua platform dan tidak menerjemahkan pasangan \r\n.
  2. while (($baris = fgets($handle)) !== false): fgets() mengembalikan false saat mencapai akhir berkas; perbandingan !== false penting karena baris kosong bernilai string, bukan boolean.
  3. Blok finally: fclose() dijamin berjalan meskipun terjadi exception di tengah pembacaan — berkas tidak pernah menggantung terbuka.
  4. yield rtrim($baris, "\r\n"): setiap baris dibersihkan dari karakter baris baru sebelum diserahkan ke pemanggil, satu baris pada satu waktu.

Membaca CSV dengan Memori Tetap #

Berkas CSV besar diolah dengan pola yang sama menggunakan fgetcsv(): baris pertama dibaca sebagai header, lalu setiap baris data digabungkan dengan header lewat array_combine() menjadi array asosiatif. Hanya satu baris yang hidup di memori pada satu saat:

php
<?php
$berkas = "/tmp/stream-penjualan.csv";
file_put_contents($berkas, "produk,qty\nkopi,2\nteh,1\ngula,5\n");

$handle = fopen($berkas, "rb");
if ($handle === false) {
    throw new RuntimeException("Gagal membuka berkas: $berkas");
}

$header = fgetcsv($handle, null, ",", "\"", "\\");
$jumlahBaris = 0;
$totalQty = 0;

while (($kolom = fgetcsv($handle, null, ",", "\"", "\\")) !== false) {
    $data = array_combine($header, $kolom);
    $totalQty += (int) $data["qty"];
    $jumlahBaris++;
}
fclose($handle);

printf("Baris data: %d, total qty: %d\n", $jumlahBaris, $totalQty);
unlink($berkas);

Output yang Dihasilkan:

text
Baris data: 3, total qty: 8

Alternatif Rapi dengan SplFileObject #

SplFileObject adalah versi object-oriented dari berkas yang dapat diiterasi langsung dengan foreach. Dua flag penting menyertainya: READ_AHEAD membuat iterator membaca satu langkah ke depan untuk mengetahui kapan berkas berakhir, dan SKIP_EMPTY melewati baris kosong — tanpa keduanya, berkas yang berakhir dengan baris baru menghasilkan satu iterasi kosong di akhir:

php
<?php
$berkas = "/tmp/spl-demo.txt";
file_put_contents($berkas, "alpha\nbeta\ngamma\n");

$file = new SplFileObject($berkas, "r");
$file->setFlags(SplFileObject::READ_AHEAD | SplFileObject::SKIP_EMPTY);

foreach ($file as $baris) {
    echo trim($baris), "\n";
}

$file->seek(1);
echo "Baris ke-2: ", trim($file->current()), "\n";

unset($file);
unlink($berkas);

Output yang Dihasilkan:

text
alpha
beta
gamma
Baris ke-2: beta

Metode seek(1) melompat langsung ke baris kedua tanpa membaca baris pertama — berguna untuk berkas berformat tetap seperti header berulang. unset($file) menutup berkas sebelum unlink() menghapusnya; tanpa ini, penghapusan bisa gagal karena berkas masih ditahan. SplFileObject juga memiliki mode CSV melalui flag READ_CSV dan metode setCsvControl(), menjadikannya alternatif satu objek untuk seluruh kebutuhan berkas pada chapter ini.

Memecah Keluaran menjadi Beberapa Berkas #

Streaming tidak berhenti pada pembacaan: keluaran yang besar juga perlu dipecah agar tiap berkas tetap mudah dikirim, diarsipkan, atau diproses sistem lain. Polanya: baca sumber baris demi baris, dan setiap N baris buka berkas potongan baru:

php
<?php
$berkasSumber = "/tmp/log-demo.txt";
file_put_contents(
    $berkasSumber,
    implode("\n", array_map(fn (int $i): string => "entri-$i", range(1, 10))) . "\n"
);

$folderTujuan = "/tmp/chunk-demo";
if (!is_dir($folderTujuan)) {
    mkdir($folderTujuan);
}
foreach (glob($folderTujuan . "/bagian-*.txt") as $lama) {
    unlink($lama);
}

$ukuran = 4;
$nomorBerkas = 0;
$barisTertulis = 0;
$keluaran = null;

$masuk = fopen($berkasSumber, "rb");
while (($baris = fgets($masuk)) !== false) {
    if ($barisTertulis % $ukuran === 0) {
        if ($keluaran !== null) {
            fclose($keluaran);
        }
        $nomorBerkas++;
        $keluaran = fopen($folderTujuan . "/bagian-$nomorBerkas.txt", "wb");
    }
    fwrite($keluaran, $baris);
    $barisTertulis++;
}
fclose($keluaran);
fclose($masuk);

foreach (glob($folderTujuan . "/bagian-*.txt") as $berkasBagian) {
    printf("%s: %d baris\n", basename($berkasBagian), count(file($berkasBagian)));
}

unlink($berkasSumber);
foreach (glob($folderTujuan . "/bagian-*.txt") as $berkasBagian) {
    unlink($berkasBagian);
}
rmdir($folderTujuan);

Output yang Dihasilkan:

text
bagian-1.txt: 4 baris
bagian-2.txt: 4 baris
bagian-3.txt: 2 baris

Alur kode tersebut: penghitung $barisTertulis % $ukuran === 0 mendeteksi saat potongan baru harus dibuka, berkas keluaran lama ditutup lebih dulu agar isinya utuh, lalu baris ditulis dengan fwrite() — string baris masih membawa \n aslinya sehingga tidak perlu ditambahkan lagi. Pembersihan glob() di awal membuat skrip aman dijalankan ulang. Sepuluh baris dengan ukuran potongan 4 menghasilkan tiga berkas: 4, 4, dan 2 baris; memori yang dipakai tetap satu baris berapa pun ukuran sumbernya.

Menahan Memori dengan Batch dan GC #

Menggabungkan generator dengan pemrosesan per batch memberi satu pola kerja yang lengkap: pembacaan streaming, pemrosesan kolektif, dan pengelolaan memori. Generator bacaBatch() di bawah mengumpulkan baris hingga batch penuh lalu menyerahkan seluruh batch sekaligus; di sela-selanya, gc_collect_cycles() memaksa garbage collector membersihkan referensi siklik yang mungkin menumpuk pada objek-objek buatan tiap batch:

php
<?php
function bacaBatch(string $berkas, int $ukuran): Generator
{
    $batch = [];
    $handle = fopen($berkas, "rb");
    if ($handle === false) {
        throw new RuntimeException("Gagal membuka berkas: $berkas");
    }
    try {
        while (($baris = fgets($handle)) !== false) {
            $batch[] = trim($baris);
            if (count($batch) === $ukuran) {
                yield $batch;
                $batch = [];
            }
        }
    } finally {
        fclose($handle);
    }
    if ($batch !== []) {
        yield $batch;
    }
}

$berkas = "/tmp/gc-demo.txt";
file_put_contents($berkas, implode("\n", range(1, 25)) . "\n");

$jumlahBatch = 0;
$jumlahBaris = 0;

foreach (bacaBatch($berkas, 10) as $batch) {
    $jumlahBaris += count($batch);
    $jumlahBatch++;
    if ($jumlahBatch % 2 === 0) {
        gc_collect_cycles();
    }
}

printf("Diproses %d baris dalam %d batch\n", $jumlahBaris, $jumlahBatch);
unlink($berkas);

Output yang Dihasilkan:

text
Diproses 25 baris dalam 3 batch

Generator di atas menyerahkan tiga batch — 10, 10, dan 5 baris; sisa baris terakhir tetap diserahkan berkat pemeriksaan setelah loop. Pemanggilan gc_collect_cycles() setiap beberapa batch adalah jaring pengaman untuk kode yang membuat banyak objek saling merujuk (misalnya DTO ber-relasi); PHP sudah menjalankan garbage collection otomatis, tetapi memicunya di titik yang dapat diprediksi menjaga puncak pemakaian memori lebih rendah pada proses yang berjam-jam.

Kapan Memuat Seluruh Berkas Masih Wajar #

Streaming bukan dogma. Untuk berkas kecil — konfigurasi, template, CSV beberapa ribu baris — file_get_contents() justru lebih cepat karena satu panggilan sistem membaca seluruh berkas tanpa overhead loop per baris. Patokan praktisnya ukuran: di bawah beberapa megabyte, memuat penuh tidak masalah; di atas puluhan megabyte, atau ketika ukuran berkas tidak bisa diprediksi karena diunggah pengguna, pilihlah pola streaming sejak awal. Mengubah kode yang sudah berjalan menjadi streaming jauh lebih menyakitkan daripada memilih pola yang benar sejak baris pertama.

Satu kebutuhan yang tampak sederhana tetapi tidak tersedia fungsi bawaannya adalah membaca berkas dari belakang — misalnya mengambil 100 entri log terakhir dari berkas yang aktif ditulis. Polanya: buka berkas dengan fopen(), pindahkan pointer ke dekat akhir dengan fseek($handle, $offset, SEEK_END), baca satu blok ke belakang dengan fread(), cari pemisah baris, lalu ulangi sampai jumlah baris cukup. Karena pointer hanya bergerak per blok kecil, berkas log berukuran gigabyte dapat dibaca dari ekornya tanpa menyentuh isinya yang lain.

Ringkasan #

Streaming menyelesaikan masalah yang tidak bisa dijawab array_chunk() atau generator saja: data yang bahkan tidak muat disebut array. fgets() dan fgetcsv() membaca baris demi baris, SplFileObject membungkusnya dalam objek yang dapat diiterasi dan dilompati, pemecahan keluaran menjaga tiap berkas tetap kecil, dan gc_collect_cycles() menjaga memori tetap landai pada proses panjang. Pola-pola ini adalah bahan baku utama ketika kita merakit alur import dan export yang sesungguhnya pada Chapter Data Import/Export .

Referensi #