Sebuah situs listing properti klien menampilkan harga di setiap listing, dan permintaannya di atas kertas terdengar sepele: toggle mata uang EUR/USD di header. Saya mulai dari bagian yang paling enak dikerjakan, yaitu helper konversinya: terima jumlah numerik plus kode mata uang, kalikan kurs, kembalikan string harga dengan simbol yang benar. Fungsinya kecil, murni, gampang di-unit-test, dan semua test-nya hijau. Saya pasang toggle-nya, klik dari EUR ke USD, dan... tidak terjadi apa-apa. Setiap harga di halaman tampil persis sama seperti sebelumnya, sampai ke simbol euro-nya.
Refleks pertama saya menyalahkan lapisan yang paling dekat dengan mata: JavaScript. Mungkin event toggle tidak ke-bind, mungkin state mata uang tidak tersimpan antar halaman. Saya cek satu per satu — state berubah, cookie ke-set, halaman di-render ulang dengan konteks mata uang yang baru. Formatter sempat saya curigai lagi, tapi test-nya tetap lulus: kasih 450 dan 'USD', keluar string dolar yang benar. Setiap komponen bekerja kalau dites sendirian. Yang rusak justru tidak kelihatan di komponen mana pun.
Investigasi: semua benar, tidak ada yang jalan
Akhirnya saya berhenti menebak dan melihat apa yang benar-benar dilakukan template listing saat merender harga:
// the old render path in the listing template
$price = get_post_meta( $post_id, 'listing_price', true );
echo esc_html( $price );
var_dump( $price ); // string(6) "€450"Dua hal terbongkar sekaligus. Pertama, template tidak pernah memanggil formatter saya; dia mengoper nilai meta mentah-mentah ke halaman. Kedua, string(6) untuk sesuatu yang saya kira tiga digit. Tiga byte ekstra itu adalah simbol euro dalam UTF-8, tertanam di dalam datanya sendiri.
Akar masalah: presentasi membeku jadi data
Harga di situs ini tidak pernah disimpan sebagai angka. Bertahun-tahun, editor konten menempel harga ke field meta persis seperti yang ingin mereka lihat di halaman: '€450', '€1,200', lengkap dengan simbol dan pemisah ribuan. Kode render lama tinggal meneruskan string itu apa adanya, jadi selama bertahun-tahun semuanya "kelihatan benar" dan tidak ada alasan buat siapa pun untuk protes.
Formatter saya mengharapkan jumlah numerik plus kode mata uang. Dia tidak pernah berada di jalur render sama sekali, dan seandainya pun ada, tidak ada yang bisa dia konversi: kamu tidak bisa mengalikan kurs ke string yang glyph mata uangnya sudah terpanggang di dalam. Toggle-nya bekerja. Formatter-nya bekerja. Mereka cuma tidak pernah bertemu dengan datanya. Itulah kenapa gejalanya "tidak berefek apa-apa" dan bukan error: tidak ada yang gagal, karena tidak ada satu pun kode saya yang pernah dilewati.
Perbaikannya
Perbaikannya tiga lapis, dan urutannya penting.
Pertama, migrasi data. Skrip sekali jalan yang membaca setiap harga tersimpan, membuang simbol dan pemisah ribuan, lalu menyimpan angka murni plus field mata uang terpisah:
// one-time migration: strip symbols and separators, keep the number
$listings = get_posts( array(
'post_type' => 'listing',
'numberposts' => -1,
'fields' => 'ids',
) );
foreach ( $listings as $listing_id ) {
$raw = get_post_meta( $listing_id, 'listing_price', true );
$amount = preg_replace( '/[^0-9.]/', '', $raw ); // '€1,200' becomes '1200'
if ( '' === $amount ) {
error_log( "unparseable price on listing {$listing_id}: {$raw}" );
continue;
}
update_post_meta( $listing_id, 'listing_price', (float) $amount );
update_post_meta( $listing_id, 'listing_currency', 'EUR' );
}Nilai yang tidak bisa diparse saya log dan bereskan manual; jumlahnya cuma segelintir. Yang penting: setelah skrip ini jalan, database hanya berisi data, bukan tampilan.
Kedua, satu pintu untuk semua render harga. Setiap tempat yang menampilkan harga — kartu listing, halaman detail, hasil pencarian — sekarang wajib lewat satu helper:
function format_price( $amount, $currency ) {
$rates = array( 'EUR' => 1.0, 'USD' => 1.08 );
$symbols = array( 'EUR' => '€', 'USD' => '$' );
$converted = (float) $amount * $rates[ $currency ];
return $symbols[ $currency ] . number_format( $converted, 0, '.', ',' );
}
// the new render path: every price goes through here, no exceptions
$amount = get_post_meta( $post_id, 'listing_price', true );
$currency = current_display_currency(); // 'EUR' or 'USD' from the switcher
echo esc_html( format_price( $amount, $currency ) );Begitu jalur ini terpasang, toggle langsung "berfungsi" — padahal toggle-nya tidak saya sentuh sama sekali. Yang berubah cuma satu hal: formatter akhirnya berdiri di antara data dan halaman.
Ketiga, kunci pintunya. Migrasi tidak ada gunanya kalau minggu depan ada editor yang menempel '€450' lagi. Field harga di admin saya ubah jadi input numerik, plus sanitasi saat save supaya string berformat tidak bisa lolos lewat jalur mana pun:
// meta box field: numeric input only
printf(
'<input type="number" step="1" min="0" name="listing_price" value="%s">',
esc_attr( get_post_meta( $post->ID, 'listing_price', true ) )
);
// sanitize on save, so nothing formatted survives even a paste
add_action( 'save_post_listing', function ( $post_id ) {
if ( isset( $_POST['listing_price'] ) ) {
$clean = preg_replace( '/[^0-9.]/', '', wp_unslash( $_POST['listing_price'] ) );
update_post_meta( $post_id, 'listing_price', (float) $clean );
}
} );Pelajaran
Pelajaran intinya klasik tapi bayarnya pakai jam kerja sungguhan: simpan data, bukan presentasi. 450 itu data. '€450' itu keputusan tampilan yang membeku jadi data, dan begitu membeku, informasi di dalamnya terkubur oleh formatnya sendiri. Checklist saya sejak insiden ini:
- Kalau sebuah switcher atau toggle "tidak berefek apa-apa", jangan mulai debug dari switcher-nya. Dump nilai mentah yang tersimpan lebih dulu; bisa jadi presentasinya sudah terpanggang di sana dan kode barumu tidak pernah berada di jalurnya.
- Rutekan setiap render nilai lewat satu formatter helper. Satu saja template yang echo meta langsung, dialah yang akan bocor begitu kebutuhan berubah.
- Buat state invalid tidak bisa dimasukkan. Input numerik plus sanitasi saat save jauh lebih murah daripada migrasi kedua.
Toggle-nya sendiri tidak pernah salah. Dia cuma tidak pernah diberi angka untuk di-toggle.
