Cara Mencegah Double Booking Kamar Hotel di PHP & MySQL (Plus Hitung Okupansi, ADR & RevPAR)
Mimpi buruk pemilik homestay: dua tamu datang membawa bukti booking untuk kamar yang sama di malam yang sama. Biasanya ini terjadi karena pengecekan ketersediaan di aplikasi kurang tepat, atau dua resepsionis menyimpan reservasi bersamaan.
Di tutorial ini kita bahas cara mengecek ketersediaan kamar hotel yang benar di PHP & MySQL, mencegah double booking dengan transaksi, lalu menghitung tiga angka yang selalu ditanyakan pemilik hotel: okupansi, ADR, dan RevPAR. Contohnya diambil dari aplikasi Sistem Reservasi Hotel & Homestay yang bisa kamu download gratis di Papicode (link di akhir artikel).
1. Model tanggal menginap: [check-in, check-out)
Tamu yang menginap 19 sampai 21 Oktober memakai kamar pada malam tanggal 19 dan 20. Pada tanggal 21 ia check-out jam 12 siang, dan kamar yang sama sudah boleh diisi tamu lain yang check-in jam 2 siang.
Karena itu, rentang menginap diperlakukan sebagai [check_in, check_out): tanggal masuk ikut dihitung, tanggal keluar tidak.
CREATE TABLE reservasi (
id INT AUTO_INCREMENT PRIMARY KEY,
kamar_id INT NOT NULL,
check_in DATE NOT NULL,
check_out DATE NOT NULL, -- eksklusif
harga_malam INT NOT NULL, -- snapshot tarif saat dipesan
status ENUM('booking','checkin','checkout','batal') NOT NULL DEFAULT 'booking',
INDEX idx_kamar_tgl (kamar_id, check_in, check_out)
) ENGINE=InnoDB;
Jumlah malam cukup selisih hari:
function malam(string $in, string $out): int
{
return max(0, (int) round((strtotime($out) - strtotime($in)) / 86400));
}
echo malam('2026-10-19', '2026-10-21'); // 2
2. Query cek bentrok yang benar
Dua rentang A dan B bertumpuk jika A.check_in < B.check_out dan A.check_out > B.check_in. Pakai tanda < dan >, bukan <= atau >=, supaya tamu yang pulang dan tamu yang datang di hari yang sama tidak dianggap bentrok.
<?php
function kamar_bentrok(PDO $pdo, int $kamarId, string $in, string $out, int $kecuali = 0): ?array
{
$st = $pdo->prepare("SELECT kode, check_in, check_out FROM reservasi
WHERE kamar_id = ?
AND status IN ('booking', 'checkin') -- yang batal & sudah pulang tidak dihitung
AND id <> ? -- abaikan diri sendiri saat edit
AND check_in < ? AND check_out > ?
LIMIT 1");
$st->execute([$kamarId, $kecuali, $out, $in]);
return $st->fetch(PDO::FETCH_ASSOC) ?: null;
}
Contoh dengan reservasi lama 19 sampai 21 Oktober:
| Reservasi baru | Hasil |
|---|---|
| 17 – 19 | Aman (pulang 19, lama datang 19) |
| 18 – 20 | Bentrok |
| 20 – 23 | Bentrok |
| 21 – 22 | Aman (lama pulang 21) |
3. Daftar kamar yang kosong untuk tanggal tertentu
Untuk halaman "cek ketersediaan", balik logikanya dengan NOT EXISTS:
SELECT k.*, t.nama AS tipe, t.harga
FROM kamar k
JOIN tipe_kamar t ON t.id = k.tipe_id
WHERE k.status <> 'perbaikan'
AND NOT EXISTS (
SELECT 1 FROM reservasi r
WHERE r.kamar_id = k.id
AND r.status IN ('booking', 'checkin')
AND r.check_in < :out AND r.check_out > :in
)
ORDER BY t.harga, k.nomor;
4. Cegah race condition dengan transaksi
Pengecekan di atas masih bisa kebobolan kalau dua resepsionis menyimpan reservasi kamar yang sama pada saat yang sama. Kunci baris kamar dengan SELECT ... FOR UPDATE, lalu cek ulang bentrok di dalam transaksi:
<?php
$pdo->beginTransaction();
try {
$st = $pdo->prepare("SELECT * FROM kamar WHERE id = ? AND status <> 'perbaikan' FOR UPDATE");
$st->execute([$kamarId]);
$kamar = $st->fetch();
if (!$kamar) throw new RuntimeException('Kamar tidak tersedia.');
if ($b = kamar_bentrok($pdo, $kamarId, $in, $out)) {
throw new RuntimeException("Kamar sudah dipesan ({$b['kode']}).");
}
$pdo->prepare('INSERT INTO reservasi (kamar_id, check_in, check_out, harga_malam) VALUES (?,?,?,?)')
->execute([$kamarId, $in, $out, $kamar['harga']]);
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
echo ->();
}
Resepsionis kedua akan menunggu sampai transaksi pertama selesai, lalu pengecekannya melihat reservasi yang baru tersimpan.
5. Menghitung okupansi, ADR, dan RevPAR
Tiga KPI standar industri perhotelan:
- Okupansi = malam terjual ÷ (jumlah kamar × jumlah hari)
- ADR (Average Daily Rate) = pendapatan kamar ÷ malam terjual
- RevPAR (Revenue per Available Room) = pendapatan kamar ÷ (jumlah kamar × jumlah hari)
Bagian yang sering salah adalah menghitung malam terjual dalam periode laporan. Reservasi 30 September sampai 3 Oktober hanya menyumbang 2 malam ke laporan Oktober. Ambil irisan rentangnya:
<?php
$dari = '2026-10-01';
$akhir = '2026-11-01'; // eksklusif: sehari setelah akhir periode
$totalKamar = 15;
$hari = malam($dari, $akhir); // 31
$malamTerjual = 0;
$pendapatan = 0;
foreach ($reservasiDalamPeriode as $r) {
$a = max($r['check_in'], $dari); // irisan awal
$b = min($r['check_out'], $akhir); // irisan akhir
$m = malam($a, $b);
$malamTerjual += $m;
$pendapatan += $m * $r['harga_malam'];
}
$okupansi = $malamTerjual / ($totalKamar * $hari) * 100; // %
$adr = $malamTerjual ? $pendapatan / $malamTerjual : ;
= / ( * );
RevPAR sering jadi angka paling jujur: hotel dengan ADR tinggi tetapi kamarnya banyak kosong akan terlihat dari RevPAR yang rendah.
Ringkasan
- Simpan rentang menginap sebagai [check-in, check-out).
- Bentrok jika
check_in < out_baru AND check_out > in_baru, hanya untuk status booking/menginap. - Pakai
NOT EXISTSuntuk daftar kamar kosong. - Kunci kamar dengan FOR UPDATE lalu cek ulang di dalam transaksi.
- Hitung okupansi, ADR, dan RevPAR dari irisan malam dengan periode laporan.
Download source code lengkapnya
Semua logika di atas sudah dipakai di Sistem Reservasi Hotel & Homestay PHP Native & MySQL: kalender kamar model Gantt, cek ketersediaan, anti double booking, check-in/out, layanan tambahan, status housekeeping, invoice, dan laporan okupansi, ADR, serta RevPAR. Gratis dan langsung jalan di XAMPP.
Source code lengkapnya gratis
Lihat semua yang dibahas di artikel ini langsung berjalan.