Cara Mencegah Jadwal Booking Bentrok di PHP & MySQL (Studi Kasus Booking Futsal)
Masalah paling sering di aplikasi booking (lapangan futsal, ruang meeting, salon, studio musik) adalah jadwal dobel: dua orang membooking lapangan yang sama di jam yang sama. Biasanya ini terjadi karena query pengecekannya kurang tepat, atau karena dua kasir menyimpan booking di detik yang hampir bersamaan.
Di tutorial ini kita bahas cara mengecek jadwal bentrok yang benar di PHP & MySQL, lalu cara mencegah race condition dengan transaksi database. Contohnya diambil dari aplikasi Sistem Booking Lapangan Futsal yang bisa kamu download gratis di Papicode (link di akhir artikel).
1. Struktur tabel booking
Simpan jam mulai dan jam selesai sebagai angka jam (0–24). Format ini cocok untuk sewa per jam dan memudahkan perbandingan.
CREATE TABLE booking (
id INT AUTO_INCREMENT PRIMARY KEY,
lapangan_id INT NOT NULL,
tanggal DATE NOT NULL,
jam_mulai TINYINT NOT NULL, -- contoh: 16 = 16.00
jam_selesai TINYINT NOT NULL, -- contoh: 18 = 18.00 (eksklusif)
status ENUM('dipesan','selesai','batal') NOT NULL DEFAULT 'dipesan',
INDEX idx_jadwal (lapangan_id, tanggal)
);
Index (lapangan_id, tanggal) penting karena pengecekan bentrok selalu memfilter dua kolom itu.
2. Rumus tumpang tindih (overlap) yang benar
Banyak tutorial mengecek bentrok seperti ini:
-- SALAH: hanya mendeteksi jam mulai yang sama persis
SELECT * FROM booking WHERE lapangan_id = 1 AND tanggal = '2026-10-20' AND jam_mulai = 16;
Query di atas lolos kalau ada booking 15.00–17.00 lalu orang lain memesan 16.00–18.00, padahal keduanya bentrok di jam 16.00–17.00.
Rumus yang benar untuk dua rentang waktu A dan B: keduanya bentrok jika
A mulai < B selesai DAN A selesai > B mulai
Coba cek dengan beberapa contoh, booking lama 16–18:
| Booking baru | Mulai baru < 18? | Selesai baru > 16? | Hasil |
|---|---|---|---|
| 14–16 | ya | tidak (16 > 16 salah) | Aman (bersebelahan) |
| 15–17 | ya | ya | Bentrok |
| 17–19 | ya | ya | Bentrok |
| 15–20 | ya | ya | Bentrok (menutupi) |
| 18–20 | tidak | ya | Aman (bersebelahan) |
Karena jam selesai bersifat eksklusif, booking 14–16 dan 16–18 boleh berurutan tanpa dianggap bentrok.
3. Fungsi PHP untuk cek bentrok
<?php
function slot_bentrok(PDO $pdo, int $lapanganId, string $tanggal, int $mulai, int $selesai, int $kecualiId = 0): ?array
{
$sql = "SELECT id, jam_mulai, jam_selesai FROM booking
WHERE lapangan_id = ? AND tanggal = ?
AND status <> 'batal'
AND id <> ?
AND jam_mulai < ? -- booking lama mulai sebelum booking baru selesai
AND jam_selesai > ? -- booking lama selesai setelah booking baru mulai
LIMIT 1";
$st = $pdo->prepare($sql);
$st->execute([$lapanganId, $tanggal, $kecualiId, $selesai, $mulai]);
return $st->fetch(PDO::FETCH_ASSOC) ?: null;
}
Dua detail kecil yang sering terlupa:
status <> 'batal': booking yang dibatalkan harus membebaskan slotnya.id <> ?: saat mengedit booking, booking itu sendiri jangan dianggap bentrok dengan dirinya.
4. Cegah race condition dengan transaksi + FOR UPDATE
Fungsi di atas sudah benar, tapi masih bisa kebobolan kalau dua kasir menekan tombol simpan hampir bersamaan:
- Kasir A cek slot 16–18 → kosong
- Kasir B cek slot 16–18 → kosong (booking A belum tersimpan)
- Kasir A simpan
- Kasir B simpan → jadwal dobel
Solusinya: kunci baris lapangan dengan SELECT ... FOR UPDATE di dalam transaksi. Kasir B akan menunggu sampai transaksi Kasir A selesai, lalu pengecekannya melihat booking milik A.
<?php
try {
$pdo->beginTransaction();
// Kunci lapangan ini sampai commit/rollback
$st = $pdo->prepare("SELECT * FROM lapangan WHERE id = ? AND status = 'aktif' FOR UPDATE");
$st->execute([$lapanganId]);
$lapangan = $st->fetch(PDO::FETCH_ASSOC);
if (!$lapangan) {
throw new RuntimeException('Lapangan tidak tersedia.');
}
if ($bentrok = slot_bentrok($pdo, $lapanganId, $tanggal, $mulai, $selesai)) {
throw new RuntimeException("Jadwal bentrok dengan booking jam {$bentrok['jam_mulai']}.00–{$bentrok['jam_selesai']}.00");
}
$pdo->prepare('INSERT INTO booking (lapangan_id, tanggal, jam_mulai, jam_selesai) VALUES (?,?,?,?)')
->execute([$lapanganId, $tanggal, $mulai, $selesai]);
$pdo->commit();
echo ;
} ( ) {
->();
->();
}
Pastikan tabel memakai engine InnoDB. Engine MyISAM tidak mendukung transaksi dan
FOR UPDATE.
5. Bonus: tarif siang dan malam per jam
Arena futsal biasanya memasang tarif malam lebih mahal. Cara paling aman adalah menghitung per jam, jangan per booking, supaya booking yang melewati batas jam malam terhitung adil:
<?php
function hitung_harga(int $mulai, int $selesai, int $tarifSiang, int $tarifMalam, int $jamMalam = 17): int
{
$total = 0;
for ($h = $mulai; $h < $selesai; $h++) {
$total += $h >= $jamMalam ? $tarifMalam : $tarifSiang;
}
return $total;
}
echo hitung_harga(16, 19, 100000, 150000); // 1 jam siang + 2 jam malam = 400000
Ringkasan
- Simpan jam mulai dan jam selesai, dengan jam selesai bersifat eksklusif.
- Rumus bentrok:
mulai_lama < selesai_baru AND selesai_lama > mulai_baru. - Abaikan booking yang batal, dan abaikan booking itu sendiri saat edit.
- Bungkus cek dan simpan dalam transaksi dengan
SELECT ... FOR UPDATEsupaya aman dipakai banyak kasir. - Hitung harga per jam jika ada tarif siang dan malam.
Download source code lengkapnya
Semua logika di atas sudah dipakai di Sistem Booking Lapangan Futsal PHP Native & MySQL: grid jadwal berwarna, booking dari klik slot, DP dan pelunasan, struk, konfirmasi WhatsApp, jadwal publik untuk pelanggan, dan laporan omzet. Gratis dan langsung jalan di XAMPP.
Source code lengkapnya gratis
Lihat semua yang dibahas di artikel ini langsung berjalan.