Mengolah data dalam jumlah besar dengan array berarti menampung seluruh elemen di memori sekaligus: satu juta baris penjualan menghasilkan array berisi satu juta elemen yang semuanya hidup bersamaan. Generator menawarkan model kerja yang berbeda — sebuah fungsi yang menghasilkan nilai satu per satu hanya saat nilai tersebut diminta, lalu segera melupakan nilai itu setelah dikonsumsi. Pada chapter ini kita akan mempelajari kata kunci yield, cara kerja lazy evaluation di baliknya, pola delegasi dengan yield from, sampai pola membaca berkas besar secara bertahap. Semua contoh dapat Anda jalankan langsung dengan PHP 8.4.

Generator Pertama: Kata Kunci yield #

Generator adalah fungsi biasa yang memuat sekurang-kurangnya satu pernyataan yield. Sesuatu yang menarik terjadi ketika fungsi semacam itu dipanggil: PHP tidak menjalankan isi fungsi sama sekali, melainkan mengembalikan object dari class Generator. Isi fungsi baru dieksekusi secara bertahap saat nilai-nilainya diminta, misalnya oleh pernyataan foreach:

php
<?php
function deretAngka(int $awal, int $akhir): Generator
{
    for ($i = $awal; $i <= $akhir; $i++) {
        yield $i;
    }
}

foreach (deretAngka(1, 5) as $angka) {
    echo "Angka: $angka\n";
}

Output yang Dihasilkan:

text
Angka: 1
Angka: 2
Angka: 3
Angka: 4
Angka: 5

Mari kita telaah apa yang terjadi pada baris-baris kode di atas:

  1. Baris 2 (function deretAngka(...)): fungsi dideklarasikan dengan tipe kembalian Generator; keberadaan yield di dalamnya mengubah fungsi ini menjadi pabrik generator, bukan fungsi biasa.
  2. Baris 4-6 (for ... yield $i;): setiap kali perulangan mencapai yield, eksekusi berhenti sejenak dan nilai $i diserahkan kepada pemanggil.
  3. Baris 9 (foreach (deretAngka(1, 5) as $angka)): pemanggilan deretAngka(1, 5) langsung mengembalikan object Generator dalam keadaan belum berjalan — belum ada satu pun angka yang dihitung.
  4. Baris 9-11: foreach meminta nilai satu per satu; setiap permintaan melanjutkan eksekusi dari titik yield terakhir sampai fungsi selesai.

Proses bolak-balik ini disebut lazy evaluation: nilai dihitung hanya ketika dibutuhkan, bukan di awal. Class Generator mengimplementasikan interface Iterator, sehingga hasil generator langsung kompatibel dengan foreach tanpa penyesuaian apa pun — mekanisme di balik kompatibilitas ini akan kita bedah pada chapter Iterators.

Mengapa Generator Hemat Memori #

Array bersifat eager: range(1, 1_000_000) membangun satu juta elemen dan menyimpan semuanya di memori sebelum perulangan pertama pun berjalan. Generator bersifat lazy: pada satu saat hanya ada satu nilai yang sedang diproduksi. Karena angka memori berbeda-beda antar lingkungan, kita bandingkan dulu hasil kedua pendekatan secara fungsional:

php
<?php
function deretAngka(int $awal, int $akhir): Generator
{
    for ($i = $awal; $i <= $akhir; $i++) {
        yield $i;
    }
}

$totalArray = 0;
foreach (range(1, 1_000_000) as $angka) {
    $totalArray += $angka;
}

$totalGenerator = 0;
foreach (deretAngka(1, 1_000_000) as $angka) {
    $totalGenerator += $angka;
}

echo $totalArray === $totalGenerator ? "Hasil identik: $totalArray\n" : "Hasil berbeda\n";

Output yang Dihasilkan:

text
Hasil identik: 500000500000

Kedua versi menghasilkan jumlah yang sama persis, tetapi cara keduanya memakai memori sangat berbeda. Versi range() menyiapkan array berisi satu juta integer — pada build 64-bit ukurannya menyentuh puluhan megabyte — sedangkan versi generator hanya menyimpan satu integer pada satu waktu beserta sedikit status internal. Anda dapat memverifikasinya sendiri dengan memanggil memory_get_usage() sebelum dan sesudah perulangan; angka pastinya bervariasi antar mesin sehingga tidak kami cetak sebagai output di sini. Prinsipnya sederhana: makin sedikit nilai yang perlu hidup bersamaan, makin ringan program Anda.

Pasangan Kunci dan Nilai #

yield juga dapat menghasilkan pasangan kunci dan nilai dengan sintaks yang sama seperti penulisan elemen array, yaitu yield kunci => nilai. Pola ini berguna ketika data Anda alami berbentuk pemetaan, misalnya kategori menu restoran:

php
<?php
function daftarMenu(): Generator
{
    yield 'makanan' => 'Nasi Goreng';
    yield 'minuman' => 'Es Teh Manis';
    yield 'cemilan' => 'Pisang Goreng';
}

foreach (daftarMenu() as $kategori => $item) {
    echo "$kategori: $item\n";
}

Output yang Dihasilkan:

text
makanan: Nasi Goreng
minuman: Es Teh Manis
cemilan: Pisang Goreng

foreach menangkap kunci dan nilai secara bersamaan melalui as $kategori => $item. Kunci yang dihasilkan generator mengikuti kunci yang Anda berikan pada yield, termasuk kunci string seperti makanan — tidak ada konversi otomatis ke angka. Bentuk ini membuat generator bisa menggantikan fungsi yang tadinya membangun array asosiatif penuh.

Delegasi dengan yield from #

Ketika satu generator perlu menggabungkan nilai dari beberapa sumber, yield from mendelegasikan iterasi ke sumber tersebut sampai habis, lalu melanjutkan ke pernyataan berikutnya. Sumber dalam boleh berupa array, generator lain, maupun object iterator apa pun:

php
<?php
function nomorUtama(): Generator
{
    yield 1;
    yield 2;
}

function nomorLengkap(): Generator
{
    yield 0;
    yield from nomorUtama();
    yield from [10, 20];
    yield from new ArrayIterator([100, 200]);
}

$hasil = [];
foreach (nomorLengkap() as $nomor) {
    $hasil[] = $nomor;
}

echo implode(', ', $hasil) . "\n";

Output yang Dihasilkan:

text
0, 1, 2, 10, 20, 100, 200

Generator nomorLengkap() menyusun nilai dari tiga sumber berbeda: generator nomorUtama(), array literal [10, 20], dan object ArrayIterator. Tanpa yield from, Anda akan memerlukan perulangan foreach tersarang untuk setiap sumber — kode yang jauh lebih bising untuk hasil yang sama.

Nilai Kembali dari Generator #

Pernyataan return di dalam generator tidak menjadikan fungsi penghasil nilai secara langsung; pernyataan tersebut menghentikan generator dan menyimpan nilai kembalian di dalamnya. Nilai tersimpan tersebut baru bisa dibaca melalui metode getReturn() setelah iterasi selesai:

php
<?php
function kumpulkanHarga(): Generator
{
    $total = 0;
    foreach ([15000, 25000, 10000] as $harga) {
        $total += $harga;
        yield $harga;
    }
    return $total;
}

$transaksi = kumpulkanHarga();
foreach ($transaksi as $harga) {
    echo "Item: Rp$harga\n";
}

echo 'Total: Rp' . $transaksi->getReturn() . "\n";

Output yang Dihasilkan:

text
Item: Rp15000
Item: Rp25000
Item: Rp10000
Total: Rp50000

Selama iterasi berlangsung, yield menyerahkan setiap harga kepada foreach. Setelah nilai terakhir dikonsumsi, pernyataan return $total; berjalan dan menyimpan total 50000 di dalam generator, lalu $transaksi->getReturn() membacanya. Pola yield untuk data ditambah return untuk ringkasan ini lazim dipakai untuk agregasi: data mengalir satu per satu, statistik akhir tersedia setelahnya.

Generator Hanya Bisa Dijalankan Sekali #

Generator bersifat sekali pakai (one-shot): setelah eksekusi mencapai akhir fungsi, generator menutup dirinya dan tidak bisa diputar ulang. Mencoba mengiterasi generator yang sudah selesai langsung melempar Exception:

php
<?php
function deretAngka(int $awal, int $akhir): Generator
{
    for ($i = $awal; $i <= $akhir; $i++) {
        yield $i;
    }
}

$deret = deretAngka(1, 3);
foreach ($deret as $angka) {
    echo "Putaran pertama: $angka\n";
}

try {
    foreach ($deret as $angka) {
        echo "Putaran kedua: $angka\n";
    }
} catch (Exception $e) {
    echo 'Exception: ' . $e->getMessage() . "\n";
}

Output yang Dihasilkan:

text
Putaran pertama: 1
Putaran pertama: 2
Putaran pertama: 3
Exception: Cannot traverse an already closed generator

Perulangan kedua tidak sempat mencetak apa pun karena foreach memanggil rewind() secara internal, dan rewind() pada generator yang telah berjalan dilarang. Jika Anda membutuhkan datanya dua kali, panggil fungsinya sekali lagi untuk mendapatkan generator baru, atau kumpulkan sekali dengan iterator_to_array() lalu pakai array hasilnya berulang kali.

Membaca Berkas Besar Secara Bertahap #

Manfaat paling nyata dari generator adalah memproses berkas besar tanpa memuat seluruh isinya ke memori. file_get_contents() membaca seluruh berkas sekaligus; kombinasi fopen() dan fgets() membaca baris demi baris, dan generator menjadi pembungkus yang rapi untuk pola tersebut. Contoh berikut membuat berkas kecil terlebih dahulu agar dapat dijalankan di mana saja, tetapi polanya sama untuk berkas berukuran ratusan megabyte:

php
<?php
$path = sys_get_temp_dir() . '/penjualan-harian.txt';
file_put_contents($path, "Budi|15000\nSiti|25000\nAndi|10000\n");

function bacaBaris(string $path): Generator
{
    $berkas = fopen($path, 'r');
    try {
        while (($baris = fgets($berkas)) !== false) {
            yield rtrim($baris, "\n");
        }
    } finally {
        fclose($berkas);
    }
}

$totalPenjualan = 0;
foreach (bacaBaris($path) as $baris) {
    [$nama, $nominal] = explode('|', $baris);
    echo "$nama: Rp$nominal\n";
    $totalPenjualan += (int) $nominal;
}

echo "Total: Rp$totalPenjualan\n";
unlink($path);

Output yang Dihasilkan:

text
Budi: Rp15000
Siti: Rp25000
Andi: Rp10000
Total: Rp50000

Mari kita telaah bagian-bagiannya:

  1. Baris 2-3: berkas contoh dibuat dengan file_put_contents() agar contoh ini mandiri; sys_get_temp_dir() menunjuk direktori sementara sistem sehingga contoh tidak menulis ke lokasi tetap.
  2. Baris 5-15 (function bacaBaris(...)): generator membuka berkas, lalu fgets() mengambil satu baris pada satu waktu; setiap yield menyerahkan baris tersebut setelah dibersihkan dari karakter baris baru.
  3. Baris 9 (while (($baris = fgets($berkas)) !== false)): fgets() mengembalikan false saat mencapai akhir berkas, sehingga perulangan berhenti secara alami.
  4. Blok finally (baris 12-14): fclose() dijamin berjalan baik saat iterasi tuntas maupun saat konsumen berhenti lebih awal, sehingga file handle tidak bocor.
  5. Baris 18-22: foreach mengonsumsi baris satu per satu; pada satu waktu hanya ada satu baris string di memori.

Karena bacaBaris() hanya menahan satu baris pada satu waktu, memori yang dipakai tidak bertambah seiring besarnya berkas. Batas praktisnya bukan memori, melainkan waktu baca disk — dan Anda bisa menghentikan konsumsi kapan saja (misalnya dengan break) tanpa membaca sisa berkas.

Ringkasan #

Generator mengubah cara kita berpikir tentang kumpulan data: dari “kumpulkan semuanya dulu” menjadi “produksi saat dibutuhkan”. Kata kunci yield menjeda fungsi dan menyerahkan nilai; yield from mendelegasikan ke sumber lain; return menyimpan nilai akhir yang dibaca lewat getReturn(); dan sifat sekali pakai menuntut kita membuat generator baru untuk setiap perulangan. Di balik layar, semua kemudahan ini berdiri di atas protokol Iterator — protokol yang akan kita implementasikan dengan tangan sendiri pada chapter berikutnya.

Referensi #