PHP memberi kita if, match, while, for, foreach, break, continue, bahkan goto — tetapi bahasa tidak mengajarkan cara menyusun semuanya. Dua program yang berperilaku identik bisa berbeda drastis dalam tingkat kesulitan dibaca dan dirawat; perbedaannya bukan pada sintaks, melainkan pada keputusan desain. Chapter ini tidak memperkenalkan sintaks baru sama sekali. Fokusnya adalah keahlian merancang alur: bagaimana menyusun percabangan dan perulangan agar pembaca enam bulan ke depan — termasuk diri kita sendiri — memahami niat kode dengan cepat. Kita akan membedah prinsip keterbacaan satu per satu, lalu menutup dengan refactor menyeluruh dari sebuah function yang berantakan.

Prinsip Keterbacaan Alur #

Kode dibaca jauh lebih sering daripada ditulis: sekali saat ditulis, berkali-kali saat di-debug, diperluas, dan direview. Setiap keputusan kontrol alur yang kita ambil menambah atau mengurangi beban pembaca tersebut. Lima prinsip berikut menjadi kerangka kerja kita sepanjang chapter ini:

  • Alur utama (happy path) berada di tingkat indentasi teratas.
  • Kondisi kegagalan ditangani sedini mungkin, lalu keluar segera.
  • Kedalaman nesting dijaga seminimal mungkin.
  • Kondisi yang kompleks diberi nama yang menjelaskan maksudnya.
  • Satu function mengerjakan satu tugas yang bisa dinamai dengan jelas.

Guard Clause dan Early Return #

Masalah paling umum pada kode pemula adalah validasi berantai yang menumpuk: setiap kondisi gagal menggeser kode utama makin jauh ke dalam. Teknik pemecahnya disebut guard clause — periksa kondisi penolakan di pintu masuk, keluar segera jika gagal, dan biarkan alur utama tetap datar. Mari kita bandingkan dua versi function yang berperilaku persis sama:

php
<?php
function bisaMembayarBuruk(array $pengguna, int $total): string
{
    if ($pengguna["aktif"]) {
        if ($total > 0) {
            if ($pengguna["saldo"] >= $total) {
                return "Transaksi diproses";
            } else {
                return "Saldo tidak cukup";
            }
        } else {
            return "Total tidak valid";
        }
    } else {
        return "Akun tidak aktif";
    }
}

function bisaMembayarJernih(array $pengguna, int $total): string
{
    if (!$pengguna["aktif"]) {
        return "Akun tidak aktif";
    }
    if ($total <= 0) {
        return "Total tidak valid";
    }
    if ($pengguna["saldo"] < $total) {
        return "Saldo tidak cukup";
    }
    return "Transaksi diproses";
}

$kasus = [
    [["aktif" => true, "saldo" => 50000], 30000],
    [["aktif" => true, "saldo" => 10000], 30000],
    [["aktif" => false, "saldo" => 50000], 30000],
    [["aktif" => true, "saldo" => 50000], 0],
];

foreach ($kasus as $i => [$pengguna, $total]) {
    echo "Kasus " . ($i + 1) . " buruk : " . bisaMembayarBuruk($pengguna, $total) . "\n";
    echo "Kasus " . ($i + 1) . " jernih: " . bisaMembayarJernih($pengguna, $total) . "\n";
}

Output yang Dihasilkan:

text
Kasus 1 buruk : Transaksi diproses
Kasus 1 jernih: Transaksi diproses
Kasus 2 buruk : Saldo tidak cukup
Kasus 2 jernih: Saldo tidak cukup
Kasus 3 buruk : Akun tidak aktif
Kasus 3 jernih: Akun tidak aktif
Kasus 4 buruk : Total tidak valid
Kasus 4 jernih: Total tidak valid

Versi bisaMembayarBuruk menumpuk tiga if bersarang; kode yang paling penting — return "Transaksi diproses" — terkubur di kedalaman tiga tingkat, dan setiap else berpasangan dengan if yang berjarak puluhan baris di atasnya. Versi bisaMembayarJernih membalik setiap kondisi: begitu syarat gagal, return keluar seketika. Alur utama terbaca dari atas ke bawah seperti daftar periksa, dan empat baris terakhir pengujian membuktikan kedua versi mengembalikan hasil yang sama persis untuk seluruh kasus. Refactor seperti ini selalu aman jika setiap jalur keluar diuji sebelum dan sesudah perombakan.

Meratakan Loop dengan continue #

Prinsip yang sama berlaku di dalam perulangan: tangani kondisi pengecualian di awal blok dengan continue, sehingga alur utama tidak tergeser ke dalam. Perhatikan dua versi loop saring belanja berikut:

php
<?php
$barang = [
    ["nama" => "Buku", "stok" => 4, "harga" => 25000],
    ["nama" => "Pena", "stok" => 0, "harga" => 5000],
    ["nama" => "Papan Tulis", "stok" => 2, "harga" => 150000],
];
$batasAnggaran = 100000;

foreach ($barang as $item) {
    if ($item["stok"] > 0) {
        if ($item["harga"] <= $batasAnggaran) {
            echo "{$item['nama']}: dibeli\n";
        } else {
            echo "{$item['nama']}: di atas anggaran, dilewati\n";
        }
    } else {
        echo "{$item['nama']}: stok habis, dilewati\n";
    }
}

Output yang Dihasilkan:

text
Buku: dibeli
Pena: stok habis, dilewati
Papan Tulis: di atas anggaran, dilewati

Versi di atas menyaring dua kondisi dengan if bersarang. Sekarang versi yang meratakan alurnya memakai continue:

php
<?php
$barang = [
    ["nama" => "Buku", "stok" => 4, "harga" => 25000],
    ["nama" => "Pena", "stok" => 0, "harga" => 5000],
    ["nama" => "Papan Tulis", "stok" => 2, "harga" => 150000],
];
$batasAnggaran = 100000;

foreach ($barang as $item) {
    if ($item["stok"] <= 0) {
        echo "{$item['nama']}: stok habis, dilewati\n";
        continue;
    }
    if ($item["harga"] > $batasAnggaran) {
        echo "{$item['nama']}: di atas anggaran, dilewati\n";
        continue;
    }
    echo "{$item['nama']}: dibeli\n";
}

Output yang Dihasilkan:

text
Buku: dibeli
Pena: stok habis, dilewati
Papan Tulis: di atas anggaran, dilewati

Output kedua versi identik — ketiga barang menghasilkan keputusan yang sama. Perbedaannya ada pada bentuk: versi kedua membaca tiga keputusan secara berurutan dari kiri atas ke kanan bawah, satu alasan penolakan per blok, dan aksi utama dibeli berada di indentasi teratas. continue di sini bekerja persis seperti guard clause pada function, hanya saja konteksnya iterasi — teknik yang telah kita bahas pada chapter break dan continue .

Memberi Nama pada Kondisi #

Ekspresi boolean yang panjang memaksa pembaca menerjemahkannya setiap kali bertemu. Solusi pertamanya sederhana: simpan ekspresi ke variabel yang namanya menyatakan maknanya:

php
<?php
$umur = 20;
$punyaKtp = true;

$cukupUmur = $umur >= 17;
$bolehMendaftar = $cukupUmur && $punyaKtp;

if ($bolehMendaftar) {
    echo "Pendaftaran diterima.\n";
} else {
    echo "Pendaftaran ditolak.\n";
}

Output yang Dihasilkan:

text
Pendaftaran diterima.

Kini if ($bolehMendaftar) terbaca seperti kalimat, dan detail syaratnya tinggal diperiksa sekali di tempat definisi. Untuk kondisi yang dipakai berulang atau butuh penjelasan panjang, bentuk yang lebih kuat adalah function — nama function menjadi dokumentasi, dan detail aturannya bersembunyi di satu tempat:

php
<?php
function adalahKabisat(int $tahun): bool
{
    return ($tahun % 4 === 0 && $tahun % 100 !== 0) || $tahun % 400 === 0;
}

foreach ([2024, 2025, 2000, 1900] as $tahun) {
    echo "Tahun $tahun " . (adalahKabisat($tahun) ? "kabisat" : "bukan kabisat") . "\n";
}

Output yang Dihasilkan:

text
Tahun 2024 kabisat
Tahun 2025 bukan kabisat
Tahun 2000 kabisat
Tahun 1900 bukan kabisat

Aturan kabisat sesungguhnya — habis dibagi 4, kecuali habis dibagi 100, kecuali habis dibagi 400 — kini tinggal sekali tempat. Pemanggil adalahKabisat($tahun) tidak perlu tahu detailnya, dan bila aturannya keliru, kita memperbaiki satu baris. Bandingkan dengan kode yang menempel ekspresi panjang itu langsung di dalam if pada setiap pemakaian: sekali salah salin, bug tersebar diam-diam.

Merancang Perulangan yang Jelas #

Perulangan yang sehat memiliki anatomi tiga bagian: inisialisasi akumulator sebelum loop, isi loop yang menjamin kemajuan menuju kondisi berhenti, dan perangkuman hasil setelah loop. Contoh berikut menghitung total dan rata-rata penjualan dengan anatomi tersebut:

php
<?php
$penjualan = [125000, 87000, 93000, 156000];

$total = 0;
$jumlahTransaksi = 0;

foreach ($penjualan as $nilai) {
    $total += $nilai;
    $jumlahTransaksi++;
}

$rataRata = $jumlahTransaksi > 0 ? intdiv($total, $jumlahTransaksi) : 0;

echo "Total penjualan: Rp$total\n";
echo "Jumlah transaksi: $jumlahTransaksi\n";
echo "Rata-rata: Rp$rataRata\n";

Output yang Dihasilkan:

text
Total penjualan: Rp461000
Jumlah transaksi: 4
Rata-rata: Rp115250

Ada tiga keputusan desain kecil pada contoh ini. Pertama, $total dan $jumlahTransaksi diberi nama yang menjelaskan perannya — bukan $t dan $n yang memaksa pembaca mengingat artinya. Kedua, perhitungan rata-rata diletakkan setelah loop, bukan di dalamnya, karena ia bukan bagian dari perulangan; meletakkannya di dalam membuat pembaca bertanya-tanya apakah hasilnya dihitung berulang. Ketiga, ternary $jumlahTransaksi > 0 ? ... : 0 menjaga pembagian dengan nol tetap ditangani tanpa menambah kedalaman nesting. Kombinasi ketiganya membuat loop pendek ini tetap benar meski data berubah ukuran.

Anggaran Kompleksitas #

Setiap keputusan di dalam sebuah function menambah satu titik cabang yang harus dipahami dan diuji. Hitungannya sederhana: setiap if, elseif, loop, case, &&, ||, dan ternary menambah satu ke kompleksitas. Sebuah function yang sehat umumnya berada di bawah angka sepuluh — di atas itu, pembaca mulai kehilangan jejak jalur mana yang sedang dijalankan, dan pengujian menyusut karena tidak ada yang sempat menulis kasus uji untuk semua kombinasinya. Angka ini bukan hukum, melainkan anggaran: melampauinya adalah sinyal untuk memecah function, bukan otomatis kesalahan.

Anggaran ini juga menjelaskan mengapa guard clause bekerja begitu baik: kompleksitas jumlahnya tetap, tetapi cabang-cabang berjajar datar satu tingkat alih-alih dikalikan menjadi kombinatorika bersarang yang sulit dipetakan.

Studi Kasus Refactor Lengkap #

Sekarang kita terapkan seluruh prinsip sekaligus. Berikut function validasi pesanan versi lama — lima tingkat kedalaman, else yang berpasangan jauh dari if-nya, dan perhitungan terkubur di dasar nested:

php
<?php
function prosesPesananLama(array $pesanan): string
{
    if (count($pesanan) > 0) {
        if (isset($pesanan["pelanggan"])) {
            if ($pesanan["pelanggan"] !== "") {
                if (isset($pesanan["items"])) {
                    if (count($pesanan["items"]) > 0) {
                        $total = 0;
                        foreach ($pesanan["items"] as $item) {
                            if ($item["harga"] > 0 && $item["jumlah"] > 0) {
                                $total += $item["harga"] * $item["jumlah"];
                            } else {
                                return "Item tidak valid";
                            }
                        }
                        return "Pesanan valid, total Rp$total";
                    } else {
                        return "Pesanan tanpa item";
                    }
                } else {
                    return "Item tidak ditemukan";
                }
            } else {
                return "Nama pelanggan kosong";
            }
        } else {
            return "Data pelanggan tidak ada";
        }
    } else {
        return "Pesanan kosong";
    }
}

function hitungTotalPesanan(array $items): int
{
    $total = 0;
    foreach ($items as $item) {
        $total += $item["harga"] * $item["jumlah"];
    }
    return $total;
}

function prosesPesananBaru(array $pesanan): string
{
    if (count($pesanan) === 0) {
        return "Pesanan kosong";
    }
    if (!isset($pesanan["pelanggan"])) {
        return "Data pelanggan tidak ada";
    }
    if ($pesanan["pelanggan"] === "") {
        return "Nama pelanggan kosong";
    }
    if (!isset($pesanan["items"])) {
        return "Item tidak ditemukan";
    }
    if (count($pesanan["items"]) === 0) {
        return "Pesanan tanpa item";
    }
    foreach ($pesanan["items"] as $item) {
        if ($item["harga"] <= 0 || $item["jumlah"] <= 0) {
            return "Item tidak valid";
        }
    }
    return "Pesanan valid, total Rp" . hitungTotalPesanan($pesanan["items"]);
}

$kasusUji = [
    [],
    ["items" => [["harga" => 1000, "jumlah" => 2]]],
    ["pelanggan" => "", "items" => [["harga" => 1000, "jumlah" => 2]]],
    ["pelanggan" => "Budi"],
    ["pelanggan" => "Budi", "items" => []],
    ["pelanggan" => "Siti", "items" => [["harga" => 0, "jumlah" => 3]]],
    ["pelanggan" => "Andi", "items" => [
        ["harga" => 5000, "jumlah" => 3],
        ["harga" => 8000, "jumlah" => 1],
    ]],
];

foreach ($kasusUji as $i => $uji) {
    echo "Kasus " . ($i + 1) . " lama : " . prosesPesananLama($uji) . "\n";
    echo "Kasus " . ($i + 1) . " baru: " . prosesPesananBaru($uji) . "\n";
}

Output yang Dihasilkan:

text
Kasus 1 lama : Pesanan kosong
Kasus 1 baru: Pesanan kosong
Kasus 2 lama : Data pelanggan tidak ada
Kasus 2 baru: Data pelanggan tidak ada
Kasus 3 lama : Nama pelanggan kosong
Kasus 3 baru: Nama pelanggan kosong
Kasus 4 lama : Item tidak ditemukan
Kasus 4 baru: Item tidak ditemukan
Kasus 5 lama : Pesanan tanpa item
Kasus 5 baru: Pesanan tanpa item
Kasus 6 lama : Item tidak valid
Kasus 6 baru: Item tidak valid
Kasus 7 lama : Pesanan valid, total Rp23000
Kasus 7 baru: Pesanan valid, total Rp23000

Mari kita bedah hasil refactornya. prosesPesananBaru membuka setiap syarat sebagai guard clause berurutan — pesanan kosong, pelanggan hilang, nama kosong, item hilang, item kosong — dan setiap kegagalan keluar di barisnya sendiri. Validasi item dipisah menjadi satu loop kecil yang hanya memeriksa keabsahan, sementara perhitungan total dipindahkan ke hitungTotalPesanan yang mengerjakan satu tugas; hasil akhir dirangkum dalam satu return di tingkat teratas. prosesPesananLama tetap disertakan di sebelahnya bukan sebagai teladan, melainkan sebagai pembanding — dan tujuh kasus uji di bagian bawah membuktikan kedua versi menghasilkan keluaran identik untuk seluruh cabang, termasuk kasus sukses dengan total Rp23000.

Perhatikan pula bagaimana versi baru memakai foreach untuk dua keperluan berbeda tanpa mencampurnya: satu loop memvalidasi, satu function menghitung. Prinsip “satu tugas per function” dari awal chapter terwujud di sini, dan setiap bagian cukup pendek untuk dipahami tanpa menggulir layar berkali-kali.

Berkas contoh.php pada bagian bawah halaman merangkum seluruh contoh refactor di atas dalam satu program utuh yang dapat Anda unduh dan jalankan langsung dengan php contoh.php — hasilnya identik dengan output tiap bagian chapter ini.

Ringkasan #

Merancang control flow yang baik adalah pekerjaan menjaga pembaca: alur utama di tingkat teratas lewat guard clause dan early return, loop diratakan dengan continue, kondisi kompleks diberi nama lewat variabel atau function, perulangan dijaga beranatomi jelas, dan kompleksitas dijaga dalam anggaran. Refactor besar seperti studi kasus terakhir selalu dimulai dari pengujian perilaku sebelum dan sesudah — output yang identis adalah bukti refactor tidak mengubah makna. Dengan control flow yang rapi, kita siap melangkah ke struktur data yang paling sering kita telusuri: array dan collections, pembahasan pertama Section Arrays dan Collections .

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

contoh.php

Unduh berkas
php
<?php
// contoh.php — rangkuman prinsip merancang control flow dari chapter
// "Merancang Control Flow". Jalankan dengan: php contoh.php

echo "== Guard clause dan early return ==\n";
function bisaMembayarBuruk(array $pengguna, int $total): string
{
    if ($pengguna["aktif"]) {
        if ($total > 0) {
            if ($pengguna["saldo"] >= $total) {
                return "Transaksi diproses";
            } else {
                return "Saldo tidak cukup";
            }
        } else {
            return "Total tidak valid";
        }
    } else {
        return "Akun tidak aktif";
    }
}

function bisaMembayarJernih(array $pengguna, int $total): string
{
    if (!$pengguna["aktif"]) {
        return "Akun tidak aktif";
    }
    if ($total <= 0) {
        return "Total tidak valid";
    }
    if ($pengguna["saldo"] < $total) {
        return "Saldo tidak cukup";
    }
    return "Transaksi diproses";
}

$kasus = [
    [["aktif" => true, "saldo" => 50000], 30000],
    [["aktif" => true, "saldo" => 10000], 30000],
    [["aktif" => false, "saldo" => 50000], 30000],
    [["aktif" => true, "saldo" => 50000], 0],
];

foreach ($kasus as $i => [$pengguna, $total]) {
    echo "Kasus " . ($i + 1) . " buruk : " . bisaMembayarBuruk($pengguna, $total) . "\n";
    echo "Kasus " . ($i + 1) . " jernih: " . bisaMembayarJernih($pengguna, $total) . "\n";
}

echo "\n== Loop diratakan dengan continue ==\n";
$barang = [
    ["nama" => "Buku", "stok" => 4, "harga" => 25000],
    ["nama" => "Pena", "stok" => 0, "harga" => 5000],
    ["nama" => "Papan Tulis", "stok" => 2, "harga" => 150000],
];
$batasAnggaran = 100000;

foreach ($barang as $item) {
    if ($item["stok"] <= 0) {
        echo "{$item['nama']}: stok habis, dilewati\n";
        continue;
    }
    if ($item["harga"] > $batasAnggaran) {
        echo "{$item['nama']}: di atas anggaran, dilewati\n";
        continue;
    }
    echo "{$item['nama']}: dibeli\n";
}

echo "\n== Memberi nama pada kondisi ==\n";
$umur = 20;
$punyaKtp = true;

$cukupUmur = $umur >= 17;
$bolehMendaftar = $cukupUmur && $punyaKtp;

if ($bolehMendaftar) {
    echo "Pendaftaran diterima.\n";
} else {
    echo "Pendaftaran ditolak.\n";
}

function adalahKabisat(int $tahun): bool
{
    return ($tahun % 4 === 0 && $tahun % 100 !== 0) || $tahun % 400 === 0;
}

foreach ([2024, 2025, 2000, 1900] as $tahun) {
    echo "Tahun $tahun " . (adalahKabisat($tahun) ? "kabisat" : "bukan kabisat") . "\n";
}

echo "\n== Perulangan yang jelas ==\n";
$penjualan = [125000, 87000, 93000, 156000];

$total = 0;
$jumlahTransaksi = 0;

foreach ($penjualan as $nilai) {
    $total += $nilai;
    $jumlahTransaksi++;
}

$rataRata = $jumlahTransaksi > 0 ? intdiv($total, $jumlahTransaksi) : 0;

echo "Total penjualan: Rp$total\n";
echo "Jumlah transaksi: $jumlahTransaksi\n";
echo "Rata-rata: Rp$rataRata\n";

echo "\n== Studi kasus refactor ==\n";
function prosesPesananLama(array $pesanan): string
{
    if (count($pesanan) > 0) {
        if (isset($pesanan["pelanggan"])) {
            if ($pesanan["pelanggan"] !== "") {
                if (isset($pesanan["items"])) {
                    if (count($pesanan["items"]) > 0) {
                        $total = 0;
                        foreach ($pesanan["items"] as $item) {
                            if ($item["harga"] > 0 && $item["jumlah"] > 0) {
                                $total += $item["harga"] * $item["jumlah"];
                            } else {
                                return "Item tidak valid";
                            }
                        }
                        return "Pesanan valid, total Rp$total";
                    } else {
                        return "Pesanan tanpa item";
                    }
                } else {
                    return "Item tidak ditemukan";
                }
            } else {
                return "Nama pelanggan kosong";
            }
        } else {
            return "Data pelanggan tidak ada";
        }
    } else {
        return "Pesanan kosong";
    }
}

function hitungTotalPesanan(array $items): int
{
    $total = 0;
    foreach ($items as $item) {
        $total += $item["harga"] * $item["jumlah"];
    }
    return $total;
}

function prosesPesananBaru(array $pesanan): string
{
    if (count($pesanan) === 0) {
        return "Pesanan kosong";
    }
    if (!isset($pesanan["pelanggan"])) {
        return "Data pelanggan tidak ada";
    }
    if ($pesanan["pelanggan"] === "") {
        return "Nama pelanggan kosong";
    }
    if (!isset($pesanan["items"])) {
        return "Item tidak ditemukan";
    }
    if (count($pesanan["items"]) === 0) {
        return "Pesanan tanpa item";
    }
    foreach ($pesanan["items"] as $item) {
        if ($item["harga"] <= 0 || $item["jumlah"] <= 0) {
            return "Item tidak valid";
        }
    }
    return "Pesanan valid, total Rp" . hitungTotalPesanan($pesanan["items"]);
}

$kasusUji = [
    [],
    ["items" => [["harga" => 1000, "jumlah" => 2]]],
    ["pelanggan" => "", "items" => [["harga" => 1000, "jumlah" => 2]]],
    ["pelanggan" => "Budi"],
    ["pelanggan" => "Budi", "items" => []],
    ["pelanggan" => "Siti", "items" => [["harga" => 0, "jumlah" => 3]]],
    ["pelanggan" => "Andi", "items" => [
        ["harga" => 5000, "jumlah" => 3],
        ["harga" => 8000, "jumlah" => 1],
    ]],
];

foreach ($kasusUji as $i => $uji) {
    echo "Kasus " . ($i + 1) . " lama : " . prosesPesananLama($uji) . "\n";
    echo "Kasus " . ($i + 1) . " baru: " . prosesPesananBaru($uji) . "\n";
}