Selama ini kita memakai foreach pada array, padahal foreach sebenarnya bisa menjelajahi object apa pun yang mematuhi protokol tertentu. Protokol tersebut dinamai Iterator dan menuntut lima metode yang mengatur penunjuk posisi: maju, kembali ke awal, dan memeriksa keabsahan posisi. Memahami protokol ini membuat Anda bisa membuat koleksi buatan sendiri yang terasa seperti array asli. Pada chapter ini kita mengimplementasikan Iterator secara manual, memakai jalan pintas IteratorAggregate, lalu menutup dengan utility praktis seperti ArrayIterator dan iterator_to_array().

Protokol Iterator: Lima Metode Wajib #

Interface Iterator mendefinisikan lima metode yang dipanggil foreach secara bergantian. Sebelum menulis kode, mari petakan tugas masing-masing metode:

MetodeTugas
rewind()Mengembalikan penunjuk ke posisi awal; dipanggil satu kali saat foreach dimulai
valid()Memeriksa apakah posisi sekarang masih menampung elemen
current()Mengembalikan elemen pada posisi sekarang
key()Mengembalikan kunci pada posisi sekarang
next()Memajukan penunjuk satu langkah

Ketika foreach memulai, mesin PHP memanggil rewind() satu kali, lalu berulang kali memanggil valid(); selama hasilnya true, current() dan key() dibaca, badan perulangan dijalankan, dan next() memajukan posisi. Dengan memenuhi kontrak ini, class apa pun bisa dijelajahi foreach — berikut class AntrianPesan yang menyimpan daftar status pesanan:

php
<?php
class AntrianPesan implements Iterator
{
    private array $pesan = [
        'Pesanan diterima',
        'Pesanan diproses',
        'Pesanan dikirim',
    ];
    private int $posisi = 0;

    public function current(): mixed
    {
        return $this->pesan[$this->posisi];
    }

    public function key(): int
    {
        return $this->posisi;
    }

    public function next(): void
    {
        $this->posisi++;
    }

    public function rewind(): void
    {
        $this->posisi = 0;
    }

    public function valid(): bool
    {
        return isset($this->pesan[$this->posisi]);
    }
}

foreach (new AntrianPesan() as $urutan => $status) {
    echo "$urutan: $status\n";
}

Output yang Dihasilkan:

text
0: Pesanan diterima
1: Pesanan diproses
2: Pesanan dikirim

Mari kita telaah bagian-bagian pentingnya:

  1. Baris 2 (class AntrianPesan implements Iterator): class menyatakan dirinya mematuhi protokol; PHP kini menuntut kelima metode hadir.
  2. Baris 4-9: dua property privat — $pesan berisi daftar status, $posisi adalah penunjuk indeks yang sedang berjalan.
  3. current() dan key(): mengembalikan elemen beserta kunci pada posisi sekarang; current() mendeklarasikan mixed sesuai interface, sedangkan key() boleh menyempitkan tipe kembalian menjadi int.
  4. next(): menambah $posisi satu langkah setelah setiap putaran badan perulangan.
  5. rewind(): mengembalikan $posisi ke 0; karena penunjuk bisa direset, object ini dapat di-foreach berkali-kali — berbeda dengan generator yang sekali pakai.
  6. valid(): isset() mengembalikan false ketika $posisi melewati elemen terakhir, dan itulah sinyal foreach untuk berhenti.

Karena seluruh status navigasi berada di tangan kita, class seperti ini bebas menampilkan urutan apa pun — hasil query database, baris berkas konfigurasi, hingga deret tak berujung — dengan tampilan foreach yang persis seperti array.

Jalan Pintas dengan IteratorAggregate #

Mengimplementasikan lima metode setiap kali butuh koleksi terasa berlebihan, apalagi jika data internal Anda memang berupa array. Interface IteratorAggregate menawarkan kontrak yang jauh lebih ringkas: cukup sediakan satu metode getIterator() yang mengembalikan iterator siap pakai:

php
<?php
class DaftarTugas implements IteratorAggregate
{
    public function __construct(private array $items = [])
    {
    }

    public function tambah(string $tugas): void
    {
        $this->items[] = $tugas;
    }

    public function getIterator(): ArrayIterator
    {
        return new ArrayIterator($this->items);
    }
}

$daftar = new DaftarTugas();
$daftar->tambah('Belajar iterator');
$daftar->tambah('Belajar generator');
$daftar->tambah('Menulis laporan');

foreach ($daftar as $nomor => $tugas) {
    echo ($nomor + 1) . ". $tugas\n";
}

Output yang Dihasilkan:

text
1. Belajar iterator
2. Belajar generator
3. Menulis laporan

Class DaftarTugas tidak lagi mengurus posisi sendiri; pekerjaan tersebut didelegasikan kepada ArrayIterator yang dibuat dari property $items. Ketika foreach menerima object IteratorAggregate, mesin PHP memanggil getIterator() satu kali lalu mengiterasi hasilnya. Perhatikan juga bahwa getIterator() boleh mendeklarasikan tipe kembalian yang lebih spesifik — ArrayIterator — ketimbang Traversable yang tertulis di interface, karena PHP mengizinkan penyempitan tipe kembalian.

ArrayIterator untuk Kasus Sederhana #

Tidak semua situasi membutuhkan class baru. ArrayIterator adalah iterator dari SPL yang membungkus array apa pun secara langsung, lengkap dengan dukungan count() karena class tersebut juga mengimplementasikan Countable:

php
<?php
$warna = new ArrayIterator(['merah', 'hijau', 'biru']);

foreach ($warna as $indeks => $namaWarna) {
    echo "$indeks => $namaWarna\n";
}

$warna->rewind();
echo 'Elemen pertama: ' . $warna->current() . "\n";
$warna->next();
echo 'Elemen kedua: ' . $warna->current() . "\n";
echo 'Total elemen: ' . count($warna) . "\n";

Output yang Dihasilkan:

text
0 => merah
1 => hijau
2 => biru
Elemen pertama: merah
Elemen kedua: hijau
Total elemen: 3

Tiga baris pertama output berasal dari foreach yang mengiterasi seluruh isi. Tiga baris berikutnya menunjukkan bahwa Anda juga bisa mengendarai iterator secara manual — rewind(), current(), next() — persis protokol yang dijelaskan di awal chapter. Pola manual seperti ini berguna ketika Anda hanya ingin mengintip elemen pertama tanpa mengonsumsi seluruh koleksi.

Melintasi Properti Publik Object #

foreach pada object polos — yang tidak mengimplementasikan Iterator maupun IteratorAggregate — punya perilaku bawaan: PHP melintasi seluruh property yang terlihat dari konteks pemanggil. Dari luar class, artinya hanya property public yang dijelajahi:

php
<?php
class ProdukToko
{
    public string $nama = 'Laptop Kerja';
    public int $harga = 7500000;
    private string $kodeInternal = 'LPT-001';
}

foreach (new ProdukToko() as $properti => $nilai) {
    echo "$properti: $nilai\n";
}

Output yang Dihasilkan:

text
nama: Laptop Kerja
harga: 7500000

Property $kodeInternal tidak muncul dalam output karena property tersebut private dan konteks pemanggilan (kode global) tidak berhak melihatnya. Perilaku ini kadang cukup untuk inspeksi cepat, tetapi untuk koleksi yang dirancang, protokol Iterator atau IteratorAggregate tetap pilihan yang lebih eksplisit dan dapat diprediksi.

Mengubah Iterator Menjadi Array #

Fungsi pustaka tertentu hanya menerima array, sementara sumber data Anda berupa iterator atau generator. Fungsi iterator_to_array() menjembatani keduanya dengan menyalin seluruh elemen ke array baru:

php
<?php
function paletWarna(): Generator
{
    yield 'primer' => 'Merah';
    yield 'sekunder' => 'Hijau';
    yield 'tersier' => 'Biru';
}

$sebagaiArray = iterator_to_array(paletWarna());
echo 'Primer: ' . $sebagaiArray['primer'] . "\n";
echo 'Jumlah: ' . count($sebagaiArray) . "\n";

function nilaiUlang(): Generator
{
    yield 'budi' => 70;
    yield 'siti' => 85;
    yield 'budi' => 95;
}

echo "Dengan kunci:\n";
print_r(iterator_to_array(nilaiUlang()));
echo "Tanpa kunci:\n";
print_r(iterator_to_array(nilaiUlang(), preserve_keys: false));

Output yang Dihasilkan:

text
Primer: Merah
Jumlah: 3
Dengan kunci:
Array
(
    [budi] => 95
    [siti] => 85
)
Tanpa kunci:
Array
(
    [0] => 70
    [1] => 85
    [2] => 95
)

Argumen kedua preserve_keys menentukan apakah kunci asli dipertahankan; nilai bawaannya true. Ketika iterator menghasilkan kunci yang berulang — seperti kunci 'budi' pada contoh — nilai yang lebih baru menimpa nilai yang lebih lama, sehingga 95 menggantikan 70. Mengirim preserve_keys: false membuat PHP mengabaikan kunci asli dan menata ulang nilai secara numerik 0, 1, 2, sehingga tidak ada data yang hilang.

Generator Adalah Iterator #

Class Generator yang kita pakai pada chapter sebelumnya mengimplementasikan interface Iterator yang sama. Itulah sebabnya generator bisa di-foreach tanpa upaya tambahan. Bukti paling jelas adalah mengendarai generator secara manual:

php
<?php
function hitungMundur(int $dari): Generator
{
    for ($i = $dari; $i >= 1; $i--) {
        yield $i;
    }
}

$roket = hitungMundur(3);
$roket->rewind();
while ($roket->valid()) {
    echo $roket->current() . '... ';
    $roket->next();
}
echo "Lepas!\n";

Output yang Dihasilkan:

text
3... 2... 1... Lepas!

Metode yang Anda lihat — rewind(), valid(), current(), next() — adalah empat dari lima metode protokol di awal chapter. Generator mengimplementasikannya sendiri: valid() mengembalikan false ketika eksekusi fungsi mencapai return atau akhir body, dan itulah momen foreach berhenti meminta nilai.

Kapan Perlu Iterator Custom #

Sebagian besar kebutuhan tercakup oleh tiga alat yang sudah kita bahas. Pegangan berikut membantu memilih:

SituasiPilihan yang tepat
Membungkus array milik object agar bisa di-foreachIteratorAggregate + ArrayIterator
Nilai dihitung saat diminta (rentang besar, berkas besar)Generator dengan yield
Navigasi khusus: lompat, filter, dua arah, undo posisiImplementasi Iterator manual
Membatasi atau menyaring iterator yang sudah adaIterator SPL seperti LimitIterator dan CallbackFilterIterator

Implementasi manual memberi kendali penuh atas urutan dan status, tetapi juga membuka ruang bug pada posisi batas — uji class Anda dengan koleksi kosong dan koleksi satu elemen. Jika ragu, mulailah dari generator: paling ringkas ditulis dan paling murah di memori.

Ringkasan #

Protokol Iterator adalah kontrak lima metode yang membuat object buatan Anda dapat dijelajahi foreach seperti array. IteratorAggregate meringkas pekerjaan menjadi satu metode getIterator(), ArrayIterator menyiapkan iterator tanpa class baru, foreach pada object polos melintasi property publik, dan iterator_to_array() menjembatani dunia iterator dengan dunia array. Pertanyaan berikutnya: bagaimana menuliskan tipe yang menerima array sekaligus iterator dalam satu deklarasi? Jawabannya adalah tipe iterable, topik chapter selanjutnya.

Referensi #