Generators — PHP

Generator adalah cara elegan untuk mengiterasi data TANPA memuat semuanya ke memory sekaligus. Fungsi generator menggunakan keyword yield sebagai pengganti…

Generator adalah fungsi yang menghasilkan nilainya satu per satu: dia berhenti sebentar di tiap yield, lalu lanjut saat nilai berikutnya diminta. Bayangin dua cara dapur Warung Bu Tini bekerja. Cara pertama: pagi-pagi memasak 1.000 porsi sekaligus, dan raknya penuh sebelum pembeli pertama datang. Cara kedua: memasak satu porsi setiap ada pesanan, jadi dapur cuma memegang satu piring. Fungsi yang mengembalikan array itu cara pertama. Generator itu cara kedua.

🧪 Tebakan cepat: range(1, 1_000_000) membuat array berisi sejuta angka. Kira-kira berapa memory yang dipakainya: sekitar 1 MB, 16 MB, atau 100 MB? Dan generator yang menghasilkan angka yang sama? Tebak dulu, jawabannya ada di bagian "Memory: array lawan generator".

yield: nilai satu per satu

<?php
function antrean(): Generator {
    echo "[dapur buka]\n";
    yield "Nasi Goreng";
    echo "[lanjut]\n";
    yield "Es Teh";
    echo "[dapur tutup]\n";
}

$gen = antrean();
echo "generator dibuat\n";
foreach ($gen as $menu) {
    echo "dapat: $menu\n";
}
generator dibuat
[dapur buka]
dapat: Nasi Goreng
[lanjut]
dapat: Es Teh
[dapur tutup]

Memory: array lawan generator

function angka(int $n): Generator {
    for ($i = 1; $i <= $n; $i++) {
        yield $i;
    }
}

$awal = memory_get_usage();
$data = range(1, 1_000_000);   // versi kedua: $data = angka(1_000_000);
$total = 0;
foreach ($data as $x) {
    $total += $x;
}
echo memory_get_usage() - $awal;

memory_get_usage() mengembalikan byte yang sedang dipakai PHP. Diukur di PHP 8.4:

Jumlah angka range() (array) angka() (generator)
1.000 20.536 byte 320 byte
100.000 2.113.616 byte 320 byte
1.000.000 16.793.680 byte (sekitar 16 MB) 320 byte

Jadi jawaban tebakannya: sekitar 16 MB untuk array, dan cuma 320 byte untuk generator, berapa pun jumlahnya, karena generator cuma memegang satu angka setiap saat. range(1, 10_000_000) bahkan gagal di batas memory bawaan 128 MB: PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 268435464 bytes).

Baca file besar baris per baris

Kegunaan paling nyata: file yang terlalu besar untuk dimuat sekaligus, misalnya penjualan.csv berisi sejuta baris penjualan (25,4 MB):

function bacaBaris(string $path): Generator {
    $file = fopen($path, "r");
    while (($baris = fgets($file)) !== false) {
        yield trim($baris);
    }
    fclose($file);
}

$total = 0;
foreach (bacaBaris("penjualan.csv") as $baris) {
    $total += (int) explode(",", $baris)[2];
}

Ide yang sama ada di fetch() milik PDO yang mengambil satu baris per panggilan, dan di Laravel lewat LazyCollection dan cursor().

Deret tanpa ujung

Karena nilainya baru dibuat saat diminta, generator boleh gak pernah selesai. Yang menghentikannya pemakainya:

function nomorAntrean(): Generator {
    $nomor = 1;
    while (true) {
        yield $nomor++;
    }
}

foreach (nomorAntrean() as $n) {
    if ($n > 3) {
        break;
    }
    echo "Antrean $n\n";
}
// Antrean 1, Antrean 2, Antrean 3

Deret Fibonacci juga cocok ditulis begini: mulai dari 0 dan 1, dan tiap angka berikutnya adalah jumlah dua angka sebelumnya, jadi 0 1 1 2 3 5 8 13. Simpan dua angka terakhir di variabel, yield yang pertama, lalu geser keduanya. Batasi dengan parameter $n dan loop for, supaya generatornya berhenti sendiri sesudah $n angka.

💡 Key, yield from, dan send

function hargaMenu(): Generator {
    yield "Nasi Goreng" => 15000;
    yield "Es Teh" => 5000;
}
foreach (hargaMenu() as $nama => $harga) {
    echo "$nama: $harga\n";   // Nasi Goreng: 15000, lalu Es Teh: 5000
}

⚠️ Jebakan yang sering ketemu

TL;DR: Fungsi yang berisi yield adalah generator: memanggilnya cuma membuat objek Generator, dan isinya jalan sedikit demi sedikit setiap foreach meminta nilai berikutnya, berhenti di tiap yield. Keunggulan utamanya memory: sejuta angka dari range() memakan 16.793.680 byte, generator yang sama cuma 320 byte, dan file besar bisa dibaca baris per baris dengan fgets plus yield. yield kunci => nilai memberi key, yield from meneruskan generator lain (awas key dobel di iterator_to_array), dan send() mengirim nilai masuk. Generator cuma bisa dijalani sekali dan gak bisa di-count().

Yang akan kamu pelajari