Cara Membuat Link Undangan Personal per Tamu dan Melacak Siapa yang Sudah Membuka (PHP & MySQL)
Undangan digital yang paling disukai tamu adalah undangan yang terasa pribadi: begitu dibuka, nama mereka langsung tampil di sampul. Bagi yang punya hajat, ada bonus lain: mereka bisa tahu siapa yang sudah membuka undangan dan siapa yang belum, sehingga bisa diingatkan lagi.
Di tutorial ini kita membuat fitur tersebut dengan PHP dan MySQL: kode unik per tamu, pencatatan kunjungan, tombol kirim ke WhatsApp, dan RSVP yang aman. Contohnya diambil dari Undangan Pernikahan Digital yang bisa kamu download gratis di Papicode (link di akhir artikel).
1. Tabel tamu dengan kode unik
CREATE TABLE tamu (
id INT AUTO_INCREMENT PRIMARY KEY,
acara_id INT NOT NULL,
kode CHAR(6) NOT NULL UNIQUE,
nama VARCHAR(120) NOT NULL,
wa VARCHAR(20),
maks_orang TINYINT NOT NULL DEFAULT 2,
dibuka_at DATETIME NULL, -- pertama kali link dibuka
dibuka_kali INT NOT NULL DEFAULT 0,
dikirim_at DATETIME NULL -- kapan undangan dikirim lewat WA
);
Kenapa memakai kode acak dan bukan id? Kalau link berbentuk ?tamu=15, siapa pun bisa mengganti angkanya menjadi 16, 17, dan seterusnya untuk melihat nama tamu lain. Kode acak 6 karakter sulit ditebak.
<?php
function kode_tamu(PDO $pdo): string
{
$huruf = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // tanpa 0/O dan 1/I agar tidak tertukar
do {
$k = '';
for ($i = 0; $i < 6; $i++) {
$k .= $huruf[random_int(0, strlen($huruf) - 1)];
}
$st = $pdo->prepare('SELECT 1 FROM tamu WHERE kode = ?');
$st->execute([$k]);
} while ($st->fetchColumn()); // ulangi jika (sangat jarang) sudah dipakai
return $k;
}
Gunakan random_int(), bukan rand(). random_int() memakai sumber acak yang aman untuk keperluan seperti ini.
Dengan 32 karakter dan panjang 6, ada lebih dari 1 miliar kemungkinan kode.
2. Catat saat link dibuka
Link pribadi berbentuk index.php?u=nadia-raka&k=PR8VY9. Di halaman undangan, cari tamunya lalu catat kunjungannya dalam satu query:
<?php
$st = $pdo->prepare('SELECT * FROM tamu WHERE kode = ? AND acara_id = ?');
$st->execute([strtoupper($_GET['k'] ?? ''), $acara['id']]);
$tamu = $st->fetch();
if ($tamu) {
$pdo->prepare('UPDATE tamu SET dibuka_at = COALESCE(dibuka_at, NOW()), dibuka_kali = dibuka_kali + 1 WHERE id = ?')
->execute([$tamu['id']]);
}
$nama = $tamu['nama'] ?? 'Tamu Undangan';
COALESCE(dibuka_at, NOW()) membuat dibuka_at hanya terisi sekali, yaitu saat pertama kali dibuka, sementara dibuka_kali terus bertambah. Dari dua kolom ini admin bisa menampilkan status seperti "Dibuka 3 hari lalu · 4x".
Jangan lupa tampilkan nama dengan htmlspecialchars(). Nama tamu diketik admin, tetapi tetap saja itu input yang tidak boleh langsung dicetak ke HTML.
3. Kirim lewat WhatsApp tanpa API
Tidak perlu API WhatsApp berbayar. Cukup arahkan admin ke wa.me dengan pesan yang sudah terisi. Simpan template pesannya di database agar bisa diubah tanpa menyentuh kode:
<?php
$template = "Kepada Yth.\nBapak/Ibu/Saudara/i *{nama}*\n\nKami mengundang Anda di pernikahan kami:\n*{pasangan}*\n{tanggal}\n\nDetail acara & konfirmasi kehadiran:\n{link}";
$pesan = strtr($template, [
'{nama}' => $tamu['nama'],
'{pasangan}' => 'Nadia & Raka',
'{tanggal}' => 'Jumat, 13 November 2026',
'{link}' => 'https://domainmu.com/?u=nadia-raka&k=' . $tamu['kode'],
]);
$no = preg_replace('/\D/', '', $tamu['wa']);
if (str_starts_with($no, '0')) $no = '62' . substr($no, 1); // 0812... -> 62812...
// tandai terkirim, lalu buka WhatsApp
$pdo->prepare('UPDATE tamu SET dikirim_at = COALESCE(dikirim_at, NOW()) WHERE id = ?')->execute([$tamu['id']]);
header('Location: https://wa.me/' . $no . '?text=' . rawurlencode($pesan));
exit;
Dua catatan penting:
- Pakai
strtr()dengan array untuk mengganti banyak placeholder sekaligus. Fungsi ini tidak akan mengganti ulang teks yang sudah diganti. - Pakai
rawurlencode().urlencode()mengubah spasi menjadi+, dan tanda itu ikut tampil di chat WhatsApp.
Karena endpoint ini mengubah data (menandai terkirim), lindungi dengan token CSRF di URL-nya agar tidak bisa dipicu dari situs lain.
4. RSVP satu baris per tamu
Tamu sering mengubah jawabannya, misalnya awalnya "ragu" lalu menjadi "hadir". Daripada mengecek dulu lalu memilih INSERT atau UPDATE, gunakan UNIQUE dan ON DUPLICATE KEY UPDATE:
CREATE TABLE rsvp (
id INT AUTO_INCREMENT PRIMARY KEY,
tamu_id INT NOT NULL UNIQUE,
kehadiran ENUM('hadir','tidak','ragu') NOT NULL,
jumlah TINYINT NOT NULL DEFAULT 1,
updated_at DATETIME NOT NULL
);
<?php
$kehadiran = in_array($_POST['kehadiran'] ?? '', ['hadir', 'tidak', 'ragu'], true) ? $_POST['kehadiran'] : null;
if (!$kehadiran) exit(json_encode(['ok' => false, 'msg' => 'Pilih status kehadiran.']));
// jumlah orang dibatasi jatah tamu, apa pun yang dikirim dari browser
$jumlah = $kehadiran === 'hadir' ? max(1, min((int) $tamu['maks_orang'], (int) $_POST['jumlah'])) : 0;
$pdo->prepare('INSERT INTO rsvp (tamu_id, kehadiran, jumlah, updated_at) VALUES (?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE kehadiran = VALUES(kehadiran), jumlah = VALUES(jumlah), updated_at = NOW()')
->execute([$tamu['id'], $kehadiran, $jumlah]);
Perhatikan min($tamu['maks_orang'], ...). Tamu dengan jatah 2 orang tidak bisa mengirim angka 9 lewat DevTools.
Total orang yang akan hadir cukup dihitung dengan satu query untuk keperluan katering:
SELECT SUM(r.jumlah) FROM rsvp r JOIN tamu t ON t.id = r.tamu_id
WHERE t.acara_id = 1 AND r.kehadiran = 'hadir';
5. Ucapan tanpa spam
Kolom ucapan terbuka untuk umum, jadi pasang beberapa pengaman sederhana:
<?php
// 1. Tamu terdaftar memakai nama dari database, bukan dari form
$nama = $tamu ? $tamu['nama'] : trim($_POST['nama'] ?? '');
// 2. Maksimal 3 ucapan per 10 menit per IP
$st = $pdo->prepare('SELECT COUNT(*) FROM ucapan WHERE acara_id = ? AND ip = ? AND created_at > NOW() - INTERVAL 10 MINUTE');
$st->execute([$acara['id'], $_SERVER['REMOTE_ADDR']]);
if ($st->fetchColumn() >= 3) exit(json_encode(['ok' => false, 'msg' => 'Coba lagi beberapa menit lagi.']));
// 3. Tolak ucapan yang sama persis (tombol kirim ditekan dua kali)
$st = $pdo->prepare('SELECT 1 FROM ucapan WHERE acara_id = ? AND nama = ? AND isi = ?');
$st->execute([$acara['id'], $nama, $isi]);
if ($st->fetchColumn()) exit(json_encode(['ok' => false, 'msg' => 'Ucapan yang sama sudah terkirim.']));
Saat ditampilkan lewat JavaScript, escape dulu isinya sebelum dimasukkan ke innerHTML:
const esc = s => String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
list.insertAdjacentHTML('afterbegin', '<li><b>' + esc(w.nama) + '</b><p>' + esc(w.isi) + '</p></li>');
Tanpa langkah ini, seseorang bisa mengirim ucapan berisi <script> yang akan berjalan di HP setiap tamu yang membuka undangan.
Ringkasan
- Gunakan kode acak dari
random_int()untuk link tamu, bukan ID berurutan. COALESCE(dibuka_at, NOW())mencatat waktu pertama dibuka, sedangkandibuka_kali + 1menghitung kunjungan.- Kirim lewat
wa.me+rawurlencode(), lalu tandai terkirim dengan endpoint yang dilindungi CSRF. - RSVP memakai
UNIQUE+ON DUPLICATE KEY UPDATE, dengan jumlah orang dibatasi di server. - Ucapan dibatasi per IP, nama tamu dikunci, dan isinya di-escape sebelum ditampilkan.
Download source code lengkapnya
Semua teknik di atas sudah dipakai di Undangan Pernikahan Digital PHP Native & MySQL: banyak undangan dalam satu aplikasi, impor tamu massal, kirim WA sekali klik, status dibuka, RSVP, ucapan live dengan moderasi, amplop digital, musik, galeri, countdown, simpan ke kalender, dan 4 palet warna. Gratis dan langsung jalan di XAMPP.
Source code lengkapnya gratis
Lihat semua yang dibahas di artikel ini langsung berjalan.
Tutorial lainnya
Cara Membuat Nomor Antrean Harian yang Aman dari Duplikat di Laravel (Studi Kasus Aplikasi Kasir)
20 Oktober 2026
Cara Membuat Absensi Online dengan GPS dan Selfie di Google Apps Script
20 Oktober 2026
Cara Membuat Checkout Order via WhatsApp di WordPress Tanpa WooCommerce
19 Oktober 2026