Bug paling murah adalah yang ditemukan sebelum kode pernah berjalan; bug paling mahal adalah yang ditemukan oleh pengguna di produksi. Di antara keduanya ada unit test, code review, dan satu teknik yang bekerja bahkan sebelum program dieksekusi: static analysis. Setelah sepanjang section ini kita menulis type declaration dan anotasi PHPDoc, kini saatnya melihat alat yang benar-benar menagih kontrak-kontrak tersebut. Pada Chapter ini kita akan memahami cara kerja static analysis, jenis bug yang bisa ditemukannya (dengan contoh nyata), batasnya, serta peta alat PHPStan, Psalm, dan IDE.
Apa Itu Static Analysis #
Static analysis adalah pemeriksaan kode sumber tanpa menjalankannya. Sebuah static analyzer membaca seluruh berkas proyek, membangun model simbol (class, function, variabel beserta tipenya) dari type declaration native dan anotasi PHPDoc, lalu menelusuri setiap cabang if, match, dan loop untuk menyimpulkan tipe tiap ekspresi di titik mana pun. Ketika ekspresi bertentangan dengan kontrak — memanggil method yang tidak ada, mengirim string ke parameter int — analyzer melaporkannya sebagai error.
Perbedaan mendasar dengan unit test terletak pada cara menempuh jalur eksekusi. Test membuktikan jalur yang diuji benar untuk data uji yang diberikan; jalur dan data yang tidak diuji tetap gelap. Analyzer tidak butuh data sama sekali — analyzer menelusuri semua jalur secara simbolis, sehingga cabang yang jarang tersentuh pun diperiksa. Keduanya saling melengkapi, bukan saling menggantikan.
Kabar baiknya: seluruh bekal section ini langsung terpakai. Type declaration adalah bahan bakar utama analyzer, dan PHPDoc dari Chapter sebelumnya mengisi celah yang native tidak mampu nyatakan. Semakin kaya informasi tipe, semakin tajam kesimpulan analyzer.
Jenis Bug yang Ditemukan Sebelum Kode Berjalan #
Empat keluarga bug berikut adalah makanan pokok static analyzer. Setiap contoh di bawah ini adalah PHP yang benar-benar dapat dijalankan — kita sengaja memicu kegagalannya di runtime untuk melihat apa yang sebenarnya dicegah oleh analyzer.
Method yang Tidak Ada #
Salah ketik nama method adalah bug klasik yang baru muncul saat baris tersebut tereksekusi:
<?php
class Pelanggan
{
public function namaLengkap(): string
{
return 'Budi Santoso';
}
}
$pelanggan = new Pelanggan();
try {
echo $pelanggan->namaDepan(), "\n";
} catch (Error $e) {
echo 'Tertangkap di runtime: ', $e->getMessage(), "\n";
}Output yang Dihasilkan:
Tertangkap di runtime: Call to undefined method Pelanggan::namaDepan()Pemanggilan $pelanggan->namaDepan() melempar Error fatal karena class Pelanggan hanya memiliki namaLengkap(). Kita menangkapnya dengan try-catch agar program tetap hidup untuk menunjukkan pesannya. PHPStan dan Psalm mendeteksi kasus ini dalam hitungan detik tanpa menjalankan satu baris pun, karena keduanya memiliki daftar lengkap method milik Pelanggan dari hasil pembacaan class.
Nilai Kembali yang Salah Tipe #
Kontradiksi antara type declaration dan isi function terdeteksi runtime sebagai TypeError:
<?php
declare(strict_types=1);
function ambilNamaPembeli(): string
{
return 42;
}
try {
echo ambilNamaPembeli(), "\n";
} catch (TypeError $e) {
echo 'Tertangkap di runtime: ', $e->getMessage(), "\n";
}Output yang Dihasilkan:
Tertangkap di runtime: ambilNamaPembeli(): Return value must be of type string, int returnedFunction ambilNamaPembeli() berjanji mengembalikan string, tetapi baris return 42 mengirim int. Pada mode strict_types, interpreter menolaknya begitu return dieksekusi. Analyzer tidak perlu menunggu pemanggilan pertama — kecocokan antara : string dan ekspresi 42 sudah bisa dinilai langsung dari kode sumber.
Variabel yang Salah Ketik #
Tidak semua bug berakhir fatal. Salah ketik variabel hanya memicu Warning dan nilai null:
<?php
set_error_handler(
function (int $level, string $pesan): bool {
echo "Peringatan tertangkap: $pesan\n";
return true;
}
);
$namaPengguna = 'Siti';
echo $namaPenggun;Output yang Dihasilkan:
Peringatan tertangkap: Undefined variable $namaPenggunBaris terakhir membaca $namaPenggun — variabel yang tidak pernah didefinisikan karena huruf a tertinggal. PHP 8 menimbalkan Warning, nilai yang dibaca adalah null, dan echo mencetak string kosong. Karena tidak ada yang crash, bug semacam ini bisa lolos bertahun-tahun. Static analyzer menandainya sejak berkas disimpan: variabel $namaPenggun tidak pernah ada di scope tersebut.
Kode yang Tidak Pernah Dijalankan #
Kode mati (dead code) sering menjadi sisa refactor yang terlupakan:
<?php
function labelStatus(string $status): string
{
return match ($status) {
'aktif' => 'AKTIF',
'nonaktif' => 'NONAKTIF',
};
echo 'Baris ini tidak pernah dijalankan';
}
echo labelStatus('aktif'), "\n";Output yang Dihasilkan:
AKTIFPernyataan echo setelah return tidak akan pernah tereksekusi — return mengakhiri function sebelum sempat sampai ke sana. Program tetap berjalan normal dan mencetak AKTIF, sehingga kode mati tidak terasa mengganggu, padahal bisa menyembunyikan niat yang tidak terealisasi (misalnya logika yang seharusnya dijalankan sebelum return). Analyzer melaporkan kode tak terjangkau sebagai gejala kesalahan logika penempatan.
Yang Tidak Bisa Ditemukan: Bug Logika #
Static analysis bekerja dengan kontrak tipe, bukan dengan maksud bisnis. Perhatikan function berikut:
<?php
declare(strict_types=1);
function hitungTotalHarga(int $hargaSatuan, int $jumlah): int
{
return $hargaSatuan - $jumlah; // bug logika: seharusnya perkalian
}
echo hitungTotalHarga(5000, 3), "\n";Output yang Dihasilkan:
4997Semua kontrak terpenuhi: parameter int, kembali int, tidak ada variabel tak didefinisikan, tidak ada method hantu. PHPStan dan Psalm akan diam seribu bahasa — dan memang seharusnya, karena 5000 - 3 = 4997 secara tipe tidak salah, secara bisnis jelas keliru (seharusnya 5000 * 3). Analyzer tidak tahu bahwa maksud function adalah mengalikan; maksud hanya bisa diuji lewat spesifikasi eksekusi, itulah wilayah unit test. Aturan praktisnya: static analysis membuktikan kode konsisten dengan tipenya, unit test membuktikan kode benar untuk maksudnya.
Sound dan Unsound #
Dalam literatur, sebuah analyzer disebut sound jika tidak pernah menyatakan kode aman ketika ada kemungkinan (sekecil apa pun) kode tersebut salah. PHP membuat soundness penuh sangat sulit: mixed dari json_decode(), eval(), variable variables, dan properti dinamis memungkinkan bentuk kode yang mustahil dipetakan secara statis. Karena itu PHPStan dan Psalm memilih jalur pragmatis: ketika tipe tidak bisa diketahui, nilai diberi tipe mixed dan diperlakukan sebagai wilayah abu-abu — analyzer tidak menebak-nebak, sehingga false positive tetap rendah.
Konsekuensinya, analyzer PHP bersifat unsound by design: ada bug yang bisa lolos (false negative), terutama ketika data mixed mengalir tanpa diperiksa. Justru karena itu level analisis yang ketat terhadap mixed — yang akan kita bahas pada Chapter Baseline dan Analysis Levels
— begitu penting: semakin sedikit mixed yang lolos, semakin dekat kita ke jaminan kebenaran tipe.
PHPStan, Psalm, dan IDE #
Tiga kategori alat membaca kontrak tipe di ekosistem PHP, masing-masing dengan wilayah kerja berbeda:
| Aspek | PHPStan | Psalm | IDE (PhpStorm, VS Code) |
|---|---|---|---|
| Bentuk | alat CLI + CI | alat CLI + CI | tertanam di editor |
| Momen pemeriksaan | saat commit / build | saat commit / build | saat mengetik |
| Cakupan | seluruh proyek, lintas berkas | seluruh proyek, lintas berkas | berkas aktif + referensi |
| Fitur khas | level 0-10, baseline, ekstensi | taint analysis keamanan, --alter | autocompletion, refactor cepat |
| Umpan balik | detik hingga menit | detik hingga menit | instan |
PHPStan dan Psalm adalah static analyzer penuh: keduanya membangun model seluruh proyek dan cocok dijalankan di CI. Psalm dibesarkan di Vimeo dengan kekuatan tambahan berupa taint analysis (menelusuri aliran data tak tepercaya hingga ke lubang keamanan) dan fitur --alter yang memperbaiki kode otomatis. IDE memberikan pemeriksaan yang lebih dangkal tetapi instan — kesalahan diketahui begitu baris selesai diketik — dan sebagian besar IDE modern memakai mesin penalaran tipe yang serupa. Kombinasi ideal dalam praktik: IDE saat menulis, analyzer di CI sebelum merge.
Adopsi Bertahap dan Biayanya #
Memasang analyzer pada proyek lama yang minim tipe sering menghasilkan ratusan bahkan ribuan temuan sekaligus. Strategi adopsi yang lazim mengikuti tangga berikut:
- Mulai dari level analisis paling longgar (PHPStan level 0) supaya hanya bug fatal yang dilaporkan.
- Bekukan temuan lama dengan baseline agar tim bisa bekerja tanpa tenggelam dalam riwayat.
- Naikkan level satu tingkat setiap kali utang temuan selesai dilunasi.
- Pasang analyzer sebagai gerbang CI: pull request yang menambah temuan baru otomatis gagal.
Biayanya nyata: waktu perbaikan awal, diskusi soal false positive, dan disiplin menjaga anotasi tetap sinkron. Imbalannya juga nyata: bug kelas typo, tipe, dan method hantu — yang biasanya baru terlihat di produksi — berpindah ke detik-detik pertama penulisan kode. Dua Chapter berikutnya mempraktikkan kedua analyzer utama secara langsung, dimulai dari PHPStan .