Step debugging hanya sekuat tempat kita memilih berhenti. Berhenti di baris yang salah berarti menekan tombol lanjut ratusan kali; berhenti di titik yang tepat berarti masalah terlihat dalam hitungan detik. Chapter ini membedah alat kendali berhenti di IDE: jenis-jenis breakpoint, conditional breakpoint yang hanya aktif pada kondisi tertentu, watch expressions yang dievaluasi ulang setiap jeda, serta tiga perintah langkah — step over, step into, dan step out. Karena seluruh materi berlangsung di dalam IDE, contoh kode di sini berfungsi sebagai peta: kita menjalankan versi CLI-nya untuk melihat perilaku yang disimulasikan, lalu membayangkan eksekusinya berhenti di titik-titik yang ditandai.

Tiga Jenis Breakpoint #

IDE modern menyediakan lebih dari satu cara memasang jeda. Tiga jenis yang paling penting untuk pekerjaan sehari-hari:

JenisPerilaku
Line breakpointBerhenti setiap kali baris tersebut akan dieksekusi
Conditional breakpointBerhenti hanya jika kondisi yang kita tulis bernilai true
Exception breakpointBerhenti saat exception dari class tertentu dilempar, sebelum catch mengolahnya

Line breakpoint adalah default: klik area di kiri nomor baris, dan titik merah muncul. Conditional breakpoint dibuat dengan menambahkan kondisi pada line breakpoint yang sama. Exception breakpoint sedikit berbeda arahnya: kita tidak menunjuk baris, melainkan class exception — ketika RuntimeException dilempar di mana pun, eksekusi langsung berhenti tepat di baris throw-nya, lengkap dengan kondisi yang memicunya.

Conditional Breakpoint untuk Loop yang Panjang #

Breakpoint biasa di dalam loop yang berjalan 10.000 kali adalah mimpi buruk: kita menekan tombol lanjut 10.000 kali. Conditional breakpoint mengatasinya dengan mengevaluasi kondisi setiap kali baris tersentuh — debugger hanya menjeda bila kondisi terpenuhi. Simulasi CLI berikut meniru perilaku tersebut dengan if; bayangkan bahwa baris echo hanya “tersentuh” ketika kondisi benar:

php
<?php
$totalBelanja = 0;

foreach ([75000, 120000, 45000] as $harga) {
    $totalBelanja += $harga;

    // Conditional breakpoint: debugger hanya berhenti jika kondisi terpenuhi
    if ($totalBelanja > 100000) {
        echo "Debugger berhenti: total sudah Rp$totalBelanja\n";
    }
}

Output yang Dihasilkan:

text
Debugger berhenti: total sudah Rp195000
Debugger berhenti: total sudah Rp240000

Kondisi $totalBelanja > 100000 terpenuhi dua kali: pertama saat total mencapai 195000, lalu lagi di 240000. Debugger yang sesungguhnya akan menjeda eksekusi pada kedua momen tersebut — dan pada tiap jeda, seluruh variabel ($harga, $totalBelanja) dapat diperiksa kondisinya saat itu. Kondisi yang lazim dipakai antara lain $totalBelanja > 100000, $i === 5000, atau count($items) === 0.

Di IDE, kondisi ini disimpan sebagai bagian dari metadata breakpoint. Bentuk penyimpanannya berbeda antar IDE, tetapi konsepnya seragam — misalnya seperti ilustrasi berikut:

json
{
    "file": "app/keranjang.php",
    "line": 42,
    "condition": "$totalBelanja > 100000"
}

Watch Expressions yang Dievaluasi Ulang #

Breakpoint memberi tahu kita kapan berhenti; watch expressions memberi tahu apa yang dipantau. Watch adalah ekspresi yang kita daftarkan di panel Watch IDE, dan nilainya dievaluasi ulang setiap kali eksekusi berhenti — di breakpoint berikutnya, di setiap langkah step, di mana pun jeda terjadi. Contoh watch yang berguna:

  • count($keranjang) — mengawasi ukuran keranjang yang menyusut di tiap iterasi;
  • $totalBelanja * 0.11 — melihat estimasi PPN tanpa mengubah kode;
  • implode(", ", array_keys($item)) — meringkas kunci-kunci array yang sedang diproses.

Kekuatan watch terletak pada kumulatifnya: pada jeda pertama nilainya satu, pada jeda berikutnya nilainya berubah, dan mata kita langsung menangkap perubahan yang tidak wajar. Seperti kondisi breakpoint, ekspresi watch sebaiknya hanya membaca nilai — jangan memodifikasi variabel atau memanggil function yang mengubah state.

Step Over, Step Into, dan Step Out #

Ketika eksekusi berhenti, tiga perintah dasar mengendalikan langkah kita berikutnya. Diagram berikut menggambarkan rantai panggilan yang akan kita jelajahi:

Misalkan eksekusi berhenti di baris pemanggilan hitungDiskon() di dalam prosesPesanan(). Tiga perintah langkah memberi tiga kemungkinan arah:

PerintahPerilaku
Step IntoMasuk ke dalam function yang dipanggil baris ini — berhenti di baris pertama hitungDiskon()
Step OverJalankan baris ini sampai selesai tanpa masuk — berhenti di baris berikutnya, kirimNotifikasi()
Step OutJalankan sisa function saat ini sampai kembali — berhenti di pemanggil yang menunggu

Aturan praktisnya sederhana. Gunakan Step Over saat kita yakin function yang dipanggil tidak bersalah. Gunakan Step Into saat titik mencurigakan berada di dalam function tersebut — misalnya mencurigai validasiKupon() menghitung salah, maka masuklah dua tingkat. Gunakan Step Out saat kita sudah selesai memeriksa sebuah function dan ingin cepat kembali ke konteks pemanggilnya, seperti keluar dari validasiKupon() dan hitungDiskon() sekaligus untuk kembali ke prosesPesanan().

Memeriksa Call Stack Saat Eksekusi Berhenti #

Setiap jeda menghadirkan panel Call Stack: daftar function yang sedang menganga menunggu hasil. Panel ini menampilkan frame persis seperti stack trace yang kita pelajari di chapter sebelumnya — frame terbaru di atas. Kode berikut menampilkan versi CLI-nya dengan debug_backtrace(), yang mengembalikan frame sebagai array:

php
<?php
function panelCallStack(): void
{
    foreach (debug_backtrace() as $urutan => $frame) {
        $fungsi = $frame["function"] ?? "{main}";
        $lokasi = isset($frame["file"])
            ? $frame["file"] . ":" . $frame["line"]
            : "(internal)";
        echo "#$urutan $fungsi() dipanggil dari $lokasi\n";
    }
}

function layananPelanggan(string $nama): void
{
    panelCallStack();
}

function halamanDashboard(): void
{
    layananPelanggan("Budi");
}

halamanDashboard();

Output yang Dihasilkan:

text
#0 panelCallStack() dipanggil dari /home/z/my-project/php-course/content/php/debugging-dan-observability/breakpoints-dan-watch-expressions/.verify_tmp.php:15
#1 layananPelanggan() dipanggil dari /home/z/my-project/php-course/content/php/debugging-dan-observability/breakpoints-dan-watch-expressions/.verify_tmp.php:20
#2 halamanDashboard() dipanggil dari /home/z/my-project/php-course/content/php/debugging-dan-observability/breakpoints-dan-watch-expressions/.verify_tmp.php:23

Output memperlihatkan tiga frame: panelCallStack() di puncak, diikuti layananPelanggan(), lalu halamanDashboard(). Di IDE, klik salah satu frame berarti “pindah kamera” ke konteks tersebut — panel Variables ikut berganti dan menampilkan variabel lokal milik frame yang dipilih. Kemampuan berganti konteks inilah yang membuat kita bisa membandingkan isi $nama di frame bawah dengan hasil antara di frame atas.

Hover, Panel Variables, dan Evaluasi Ekspresi #

Selain panel, IDE menyediakan dua cara periksa yang lebih cepat. Pertama, hover: arahkan kursor ke variabel mana pun di editor saat eksekusi berhenti, dan tooltip menampilkan nilai terkini beserta strukturnya. Kedua, panel Variables yang selalu terbuka di sisi debugger, memftar seluruh variabel lokal, parameter, dan superglobal pada frame aktif.

Untuk pertanyaan yang tidak terjawab oleh kedua panel tersebut, ada Debug Console — dikenal juga sebagai Evaluate Expression. Console ini membaca ekspresi PHP dan mengevaluasinya di dalam konteks jeda saat ini, layaknya REPL kecil yang menempel pada kondisi program:

text
$pesanan["total"] + $ongkir

Kita bisa menguji hipotesis tanpa menyentuh kode: berapa hasil jika diskonnya 20 persen? Apakah in_array($kupon, $kuponValid) true? Jawaban langsung tersedia, dan kode aplikasi tetap bersih karena seluruh eksperimen hidup di console.

Siklus Hidup Sesi Debug #

Di balik layar, IDE dan Xdebug berkomunikasi lewat protokol bernama DBGP melalui koneksi TCP ke port 9003. Siklus penuh satu sesi berjalan sebagai berikut:

  1. IDE mulai mendengarkan koneksi masuk di port 9003.
  2. Skrip atau request PHP dimulai; Xdebug menghubungi IDE dan sesi terbentuk.
  3. Breakpoint terentuh — Xdebug mengirim posisi jeda, IDE menarik isi variabel.
  4. Kita melakukan step, memasang breakpoint baru, atau mengevaluasi ekspresi; tiap perintah menjadi pesan DBGP bolak-balik.
  5. Eksekusi dilanjutkan hingga breakpoint berikutnya atau skrip selesai.

Memahami arah komunikasi ini menjelaskan banyak gejala praktis: breakpoint tak terentuh karena xdebug.mode tanpa debug, koneksi gagal karena IDE belum mendengarkan, atau sesi lambat karena tiap step menyalin data variabel besar melintasi jaringan container. Ketika debugger terasa diam, periksa urutan siklus ini satu per satu.

Ringkasan #

Breakpoint menentukan di mana eksekusi berhenti: line breakpoint untuk titik tetap, conditional breakpoint untuk kondisi terukur, dan exception breakpoint untuk mencegat lemparan. Watch expressions memantau nilai yang berubah di tiap jeda, sementara step over/into/out mengendalikan arah penelusuran mengikuti rantai panggilan. Panel Call Stack dan kemampuan berganti konteks membuat frame pemanggil bisa dibandingkan dengan frame dalam, dan Debug Console menguji ekspresi langsung di titik jeda. Semua alat ini mengawasi program yang hidup — namun begitu masalah terjadi di produksi tanpa kesempatan menjeda, kita butuh jejak yang bertahan: logging, topik chapter berikutnya.

Referensi #