D
P
0

WordPress & JavaScript

Toggle Blokir Satu Tanggal Malah Menghapus Semua? Meta JSON String Dibaca sebagai Array PHP

21 Juli 2026·4 menit baca
Toggle Blokir Satu Tanggal Malah Menghapus Semua? Meta JSON String Dibaca sebagai Array PHP

Di sebuah situs klien, sebuah platform booking rumah pesta dengan kalender ketersediaan dan dashboard pemilik, ada satu interaksi yang kelihatan sepele. Pemilik properti klik satu sel tanggal untuk menandainya "diblokir", alias tidak tersedia. Sekali klik, satu tanggal ter-toggle. Sesederhana itu di kepala saya waktu bikin.

Tapi di lapangan, setiap kali pemilik toggle satu tanggal, seluruh blokir yang sudah ada di kalender itu ikut lenyap. Dia blokir 20 Juli, dan rentang 5 sampai 12 Juli yang tadinya sudah diblokir mendadak bersih. Setiap toggle seolah berpikir kalendernya kosong, lalu menimpa semuanya.

Ada gejala kedua yang bikin makin curiga: saat kalender pertama kali dibuka, semua sel tampil seolah tidak ada yang diblokir sama sekali, padahal di database jelas ada rentang tanggal yang tersimpan. Jadi dua sisi sama-sama rusak. Menulis membuang data lama, dan membaca tidak pernah menampilkan data yang ada.

Menelusuri jejaknya

Karena tulis dan baca dua-duanya salah, saya berhenti menebak dan buka tab Network di browser. Saya klik satu sel, lalu lihat payload yang dikirim dashboard.js:

{ "date": "2026-07-20", "blocked": true }

Bersih, masuk akal untuk klik satu sel. Lalu saya cek endpoint REST yang menerimanya. Di sinilah retakan pertama kelihatan: satu-satunya endpoint yang ada menuntut payload rentang lengkap, {blocked_dates: [{start,end}, ...]}, bukan {date, blocked} per sel. Handler-nya menerima request saya, tidak menemukan field blocked_dates, menganggapnya array kosong, lalu menyimpan array kosong itu. Itu penjelasan wipe-nya.

Tapi ada lapisan kedua yang lebih halus. Saya intip cara handler membaca state yang lama sebelum menulis:

// SEBELUM: meta diperlakukan sebagai array PHP
$ranges = get_post_meta( $id, '_pc_blocked_dates', true );
foreach ( $ranges as $r ) {
    // ... tidak pernah jalan
}

get_post_meta( ..., true ) di sini mengembalikan sebuah string JSON, bukan array. Yang menulis meta ini memakai wp_json_encode, jadi bentuk tersimpannya memang string. Tapi toggle_blocked_date di class-pc-rest-availability.php membacanya seakan itu array PHP dan langsung foreach. Iterasi atas string tidak menghasilkan satu rentang pun, jadi handler yakin tidak ada blokir yang perlu dipertahankan. Bahkan seandainya shape payload benar, read yang salah ini tetap membuat setiap tulisan mulai dari nol.

Kebetulan saya pernah nulis soal wp_slash merusak JSON di post meta, jadi refleks pertama saya ke sana. Tapi kali ini backslash-nya baik-baik saja. JSON di database valid dan bisa di-decode. Masalahnya bukan data yang rusak, melainkan kode yang lupa men-json_decode string yang valid itu.

Gejala kalender yang tampil kosong ternyata cabang ketiga dari akar yang sama. Reader GET di dashboard.js menerima rentang [{start,end}], tapi memperlakukannya sebagai daftar string tanggal datar, lalu memanggil blockedDates.has(dateStr). Karena isi Set-nya objek rentang, bukan string tanggal tunggal, has() tidak pernah cocok, dan tiap sel dirender seakan tidak diblokir.

Akar masalahnya: contract drift dua sisi

Tidak ada satu baris jahat di sini. Yang terjadi adalah kontrak antara JavaScript dan REST menyimpang di tiga titik sekaligus. Pertama, dashboard.js mengirim {date, blocked} per sel padahal endpoint hanya paham array blocked_dates penuh. Kedua, handler membaca meta JSON string seolah array PHP tanpa decode, jadi selalu memulai dari state kosong. Ketiga, reader di sisi klien menyamakan rentang {start,end} dengan string tanggal tunggal, jadi pengecekan has() mustahil cocok. Masing-masing kecil, tapi bertumpuk jadi "toggle apa pun menghapus segalanya".

Perbaikannya

Saya tambah endpoint per sel yang jujur, POST /properties/{id}/availability/toggle, yang menerima {date, blocked}, dan saya benahi arah data dari ujung ke ujung:

public function toggle_blocked_date( $request ) {
    $id      = (int) $request['id'];
    $date    = sanitize_text_field( $request['date'] );
    $blocked = (bool) $request['blocked'];
 
    // Meta disimpan sebagai JSON string, bukan array PHP.
    $existing_raw = get_post_meta( $id, '_pc_blocked_dates', true );
    $ranges       = $existing_raw ? json_decode( $existing_raw, true ) : array();
 
    // Ratakan rentang {start,end} jadi hari-hari tunggal, mutasi, lalu gabung lagi.
    $days = pc_expand_ranges( $ranges );
    if ( $blocked ) {
        $days[ $date ] = true;
    } else {
        unset( $days[ $date ] );
    }
    $ranges = pc_coalesce_days( array_keys( $days ) );
 
    update_post_meta( $id, '_pc_blocked_dates', wp_json_encode( $ranges ) );
    return rest_ensure_response( array( 'blocked_dates' => $ranges ) );
}

Kunci baris demi barisnya: json_decode saat baca, ratakan rentang ke hari tunggal supaya mutasi satu tanggal gampang, gabungkan hari berurutan kembali menjadi rentang, lalu wp_json_encode saat tulis. Di sisi klien, dashboard.js menunjuk ke endpoint toggle baru dan memuai rentang GET jadi string tanggal untuk pengecekan has():

async function toggleDate(propertyId, dateStr, blocked) {
  await fetch(`/wp-json/pc/v1/properties/${propertyId}/availability/toggle`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': pcNonce },
    body: JSON.stringify({ date: dateStr, blocked }),
  });
}
 
// Reader GET: ratakan {start,end} jadi string tanggal untuk .has()
const blockedDates = new Set();
for (const range of data.blocked_dates) {
  for (let d = new Date(range.start); d <= new Date(range.end); d.setDate(d.getDate() + 1)) {
    blockedDates.add(d.toISOString().slice(0, 10));
  }
}

Endpoint array penuh yang lama tetap saya biarkan hidup untuk edit borongan. Karena keduanya menyimpan bentuk JSON yang sama, read tetap konsisten dari mana pun datanya ditulis.

Checklist

  • Kalau tulis dan baca sama-sama salah, curigai contract drift antara klien dan API, bukan satu bug tunggal.
  • Meta yang ditulis dengan wp_json_encode adalah string. Saat baca, selalu json_decode, jangan foreach langsung.
  • Cocokkan shape payload dengan yang endpoint harapkan. Klik satu sel butuh endpoint per sel, bukan menumpang endpoint array penuh.
  • Ratakan rentang ke hari tunggal untuk mutasi dan pengecekan Set.has(), lalu gabung ulang jadi rentang saat menyimpan.
  • Ini beda dari bug wp_slash. Di sini JSON-nya valid; yang lupa cuma decode-nya. Verifikasi dengan membaca payload asli di Network, bukan menebak.