Permintaannya sepele: ganti alamat email kontak yang tampil di footer dan halaman kontak sebuah situs klien. Nilainya tersimpan sebagai option di database, jadi daripada klik-klik di admin, saya tulis migrasi kecil supaya perubahannya tercatat dan bisa diulang:
$settings = get_option( 'theme_settings', array() );
$settings['contact_email'] = $new_email;
update_option( 'theme_settings', $settings );Migrasi jalan, tidak ada error, update_option balik true. Saya buka halaman settings di admin: email baru tampil rapi di field-nya. Beres, pikir saya. Lalu saya cek halaman publiknya, dan email lama masih nangkring di footer. Saya purge cache, hard refresh, buka incognito, cek dari HP. Tetap email lama.
Ini jenis bug yang paling bikin frustrasi: admin bilang X, frontend bilang Y, dan dua-duanya yakin mereka benar. Migrasi "sukses", admin membuktikannya, tapi pengunjung situs melihat dunia yang berbeda. Split-brain.
Investigasi: nilai baru itu disimpan ke mana sebenarnya
Refleks pertama saya menyalahkan cache, dan itu buang waktu setengah jam. Setelah cache terbukti bersih, saya berhenti menebak dan langsung interogasi database lewat WP-CLI:
wp option pluck theme_settings contact_email
# new-team@example.com
wp option get site_contact_email
# old-team@example.comDan di situlah jawabannya, telanjang di terminal. Nilai yang sama ternyata hidup di DUA option key berbeda. Migrasi saya mengubah theme_settings[contact_email], dan memang benar berubah. Tapi ada key kedua, site_contact_email, yang masih memegang nilai lama. Tinggal satu pertanyaan: siapa yang baca key tua itu?
grep -rn "site_contact_email" wp-content/themes/
grep -rn "get_option( 'theme_settings'" wp-content/themes/Hasil grep-nya menjelaskan semuanya. Template footer dan halaman kontak, kode yang paling tua di theme itu, membaca site_contact_email langsung lewat get_option. Sementara halaman settings di admin, hasil refactor yang lebih baru, membaca dan menulis ke array theme_settings. Admin tampil nilai baru karena dia membaca key yang saya patch. Frontend tampil nilai lama karena dia membaca key yang tidak pernah saya sentuh.
Akar masalahnya: drift sejarah
Tidak ada yang jahat di sini, cuma sejarah. Versi awal theme menyimpan email ke site_contact_email. Bertahun kemudian, sebuah refactor memperkenalkan array theme_settings yang lebih rapi, lengkap dengan halaman settings baru. Tapi refactor itu berhenti di admin: template frontend tidak pernah ikut dimigrasikan, dan tidak ada yang menghapus jalur lama. Sejak hari itu, satu nilai punya dua sumber kebenaran, dan keduanya kebetulan selalu sinkron... sampai migrasi saya menyentuh salah satunya saja.
Akar masalahnya bukan migrasi saya salah tulis. Migrasinya benar untuk key yang dia sasar. Akar masalahnya adalah arsitektur: satu nilai, dua tempat penyimpanan, nol fungsi accessor tunggal. Selama kondisi itu dibiarkan, setiap perubahan di masa depan adalah lemparan koin: kena key yang benar atau tidak.
Perbaikannya
Perbaikan jangka pendek: migrasi harus menulis ke KEDUA key, supaya siapa pun pembacanya, nilainya konsisten:
function migrate_contact_email( $new_email ) {
// canonical key
$settings = get_option( 'theme_settings', array() );
$settings['contact_email'] = $new_email;
update_option( 'theme_settings', $settings );
// legacy key: keep in sync until every reader is refactored
update_option( 'site_contact_email', $new_email );
}Tapi itu cuma menambal gejala. Perbaikan sesungguhnya adalah menghapus kondisi dua-sumber-kebenaran itu sendiri. Saya buat satu fungsi getter yang jadi satu-satunya pintu untuk membaca nilai ini, dengan key baru sebagai kanonik dan key lama sebagai fallback:
function get_contact_email() {
$settings = get_option( 'theme_settings', array() );
if ( ! empty( $settings['contact_email'] ) ) {
return $settings['contact_email'];
}
// fallback for values that only ever lived in the legacy key
return get_option( 'site_contact_email', '' );
}Lalu saya refactor semua call site, footer, halaman kontak, semuanya, supaya lewat get_contact_email(). Tidak ada lagi get_option telanjang untuk nilai ini di template. Rencana penutupnya: setelah satu siklus rilis berjalan aman, key legacy site_contact_email dihapus sekalian, dan fallback-nya menyusul.
Pelajaran
Checklist yang saya bawa pulang dari insiden ini:
Pertama, sebelum mem-patch sebuah nilai, grep dulu SEMUA tempat yang membacanya. Cari setiap panggilan get_option yang relevan, jangan berasumsi hanya ada satu key.
Kedua, kalau admin menunjukkan X tapi frontend menunjukkan Y, dan cache sudah terbukti bersih, curigai dua storage key. Itu satu-satunya cara dua sisi bisa sama-sama "benar" sambil saling bertentangan.
Ketiga, satu nilai = satu fungsi accessor. Begitu ada dua tempat penyimpanan untuk nilai yang sama, satu-satunya jalan keluar yang waras adalah satu getter kanonik dengan fallback, refactor semua pembaca, lalu bunuh key lamanya. Migrasi yang "sukses" tidak ada artinya kalau dia sukses di alamat yang salah.
