CRUD Lengkap

CRUD adalah singkatan dari Create, Read, Update, Delete β€” empat operasi dasar yang hampir selalu ada di setiap aplikasi web. Di bab ini, kita akan membuat aplikasi CRUD lengkap untuk mengelola data buku tamu.

Struktur Project

~/Herd/belajar/
β”œβ”€β”€ config/
β”‚   └── database.php         ← Koneksi database
β”œβ”€β”€ templates/
β”‚   β”œβ”€β”€ header.php            ← Header & navigasi
β”‚   └── footer.php            ← Footer
β”œβ”€β”€ buku-tamu/
β”‚   β”œβ”€β”€ index.php             ← READ β€” Daftar semua data
β”‚   β”œβ”€β”€ tambah.php            ← CREATE β€” Form tambah data
β”‚   β”œβ”€β”€ edit.php              ← UPDATE β€” Form edit data
β”‚   └── hapus.php             ← DELETE β€” Hapus data
└── style.css                 ← Styling

Persiapan

1. Database (kalau belum dibuat)

CREATE DATABASE IF NOT EXISTS belajar_web;
USE belajar_web;

CREATE TABLE IF NOT EXISTS buku_tamu (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nama VARCHAR(100) NOT NULL,
    email VARCHAR(100) NOT NULL,
    pesan TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

2. File Koneksi: config/database.php

<?php
$host = 'localhost';
$dbname = 'belajar_web';
$username = 'root';
$password = '';

try {
    $pdo = new PDO(
        "mysql:host=$host;dbname=$dbname;charset=utf8mb4",
        $username,
        $password,
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false,
        ]
    );
} catch (PDOException $e) {
    die("Koneksi gagal: " . $e->getMessage());
}

3. Template Header: templates/header.php

<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= $title ?? 'Buku Tamu' ?></title>
    <link rel="stylesheet" href="/style.css">
</head>
<body>
    <nav>
        <div class="logo">πŸ“– Buku Tamu</div>
        <ul class="menu">
            <li><a href="/buku-tamu/">Daftar Tamu</a></li>
            <li><a href="/buku-tamu/tambah.php">Tambah Baru</a></li>
        </ul>
    </nav>
    <div class="container">
    </div>
    <footer>
        <p>&copy; 2026 Buku Tamu β€” Belajar PHP CRUD</p>
    </footer>
</body>
</html>

5. Styling: style.css

* { margin: 0; padding: 0; box-sizing: border-box; }

body {
    font-family: 'Segoe UI', sans-serif;
    line-height: 1.6;
    color: #333;
    background: #f0f2f5;
}

nav {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 15px 30px;
    background: linear-gradient(135deg, #667eea, #764ba2);
    color: white;
}

.logo { font-size: 20px; font-weight: bold; }
.menu { display: flex; gap: 20px; list-style: none; }
.menu a { color: white; text-decoration: none; font-weight: 500; }
.menu a:hover { text-decoration: underline; }

.container {
    max-width: 900px;
    margin: 30px auto;
    padding: 0 20px;
}

h1 {
    margin-bottom: 20px;
    color: #333;
}

/* Tabel */
table {
    width: 100%;
    border-collapse: collapse;
    background: white;
    border-radius: 10px;
    overflow: hidden;
    box-shadow: 0 2px 10px rgba(0,0,0,0.08);
}

th { background: #667eea; color: white; padding: 12px 15px; text-align: left; }
td { padding: 12px 15px; border-bottom: 1px solid #eee; }
tr:last-child td { border-bottom: none; }
tr:hover { background: #f8f9fe; }

/* Tombol */
.btn {
    display: inline-block;
    padding: 6px 14px;
    border-radius: 6px;
    text-decoration: none;
    font-size: 14px;
    font-weight: 500;
    border: none;
    cursor: pointer;
}

.btn-primary { background: #667eea; color: white; }
.btn-primary:hover { background: #5a6fd6; }
.btn-warning { background: #ffc107; color: #333; }
.btn-warning:hover { background: #e0a800; }
.btn-danger { background: #dc3545; color: white; }
.btn-danger:hover { background: #c82333; }
.btn-success { background: #28a745; color: white; }
.btn-success:hover { background: #218838; }

/* Form */
.form-card {
    background: white;
    padding: 30px;
    border-radius: 12px;
    box-shadow: 0 4px 15px rgba(0,0,0,0.1);
    max-width: 500px;
}

.form-group { margin-bottom: 18px; }
.form-group label { display: block; margin-bottom: 5px; font-weight: 600; color: #444; }
.form-group input,
.form-group textarea {
    width: 100%;
    padding: 10px 14px;
    border: 2px solid #ddd;
    border-radius: 8px;
    font-size: 15px;
    font-family: inherit;
}
.form-group input:focus,
.form-group textarea:focus { outline: none; border-color: #667eea; }

.btn-submit {
    width: 100%;
    padding: 12px;
    background: #667eea;
    color: white;
    border: none;
    border-radius: 8px;
    font-size: 16px;
    cursor: pointer;
}
.btn-submit:hover { background: #5a6fd6; }

/* Alert */
.alert {
    padding: 12px 16px;
    border-radius: 8px;
    margin-bottom: 20px;
    font-size: 14px;
}
.alert-success { background: #e8f5e9; border: 1px solid #28a745; color: #28a745; }
.alert-error { background: #fde8e8; border: 1px solid #dc3545; color: #dc3545; }

/* Actions */
.actions { display: flex; gap: 6px; }

footer {
    text-align: center;
    padding: 20px;
    background: #333;
    color: #aaa;
    margin-top: 30px;
}

CREATE β€” Tambah Data

File buku-tamu/tambah.php:

<?php
require __DIR__ . '/../config/database.php';

$title = 'Tambah Tamu Baru';
$nama = '';
$email = '';
$pesan = '';
$errors = [];
$sukses = false;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Ambil dan sanitasi data
    $nama = htmlspecialchars(trim($_POST['nama'] ?? ''));
    $email = filter_var(trim($_POST['email'] ?? ''), FILTER_SANITIZE_EMAIL);
    $pesan = htmlspecialchars(trim($_POST['pesan'] ?? ''));

    // Validasi
    if (empty($nama)) $errors[] = "Nama wajib diisi";
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = "Email tidak valid";
    if (empty($pesan)) $errors[] = "Pesan wajib diisi";

    // Simpan ke database
    if (empty($errors)) {
        $stmt = $pdo->prepare("INSERT INTO buku_tamu (nama, email, pesan) VALUES (?, ?, ?)");
        $stmt->execute([$nama, $email, $pesan]);
        $sukses = true;

        // Reset form
        $nama = '';
        $email = '';
        $pesan = '';
    }
}

require __DIR__ . '/../templates/header.php';
?>

<h1>βž• Tambah Tamu Baru</h1>

<?php if ($sukses): ?>
    <div class="alert alert-success">
        βœ… Data berhasil ditambahkan!
        <a href="/buku-tamu/">Lihat daftar β†’</a>
    </div>
<?php endif; ?>

<?php if (!empty($errors)): ?>
    <div class="alert alert-error">
        <?php foreach ($errors as $error): ?>
            <p>⚠️ <?= $error ?></p>
        <?php endforeach; ?>
    </div>
<?php endif; ?>

<div class="form-card">
    <form method="POST">
        <div class="form-group">
            <label for="nama">Nama</label>
            <input type="text" id="nama" name="nama" value="<?= $nama ?>" required>
        </div>

        <div class="form-group">
            <label for="email">Email</label>
            <input type="email" id="email" name="email" value="<?= $email ?>" required>
        </div>

        <div class="form-group">
            <label for="pesan">Pesan</label>
            <textarea id="pesan" name="pesan" rows="5" required><?= $pesan ?></textarea>
        </div>

        <button type="submit" class="btn-submit">πŸ’Ύ Simpan</button>
    </form>
</div>

<?php require __DIR__ . '/../templates/footer.php'; ?>

READ β€” Tampilkan Data

File buku-tamu/index.php:

<?php
require __DIR__ . '/../config/database.php';

$title = 'Daftar Buku Tamu';

// Pesan sukses dari redirect
$pesan_sukses = $_GET['sukses'] ?? '';

// Ambil semua data, terbaru di atas
$stmt = $pdo->query("SELECT * FROM buku_tamu ORDER BY created_at DESC");
$bukuTamu = $stmt->fetchAll();

require __DIR__ . '/../templates/header.php';
?>

<h1>πŸ“– Daftar Buku Tamu</h1>

<?php if ($pesan_sukses === 'hapus'): ?>
    <div class="alert alert-success">βœ… Data berhasil dihapus!</div>
<?php elseif ($pesan_sukses === 'edit'): ?>
    <div class="alert alert-success">βœ… Data berhasil diupdate!</div>
<?php endif; ?>

<p style="margin-bottom: 15px;">
    <a href="tambah.php" class="btn btn-primary">βž• Tambah Tamu Baru</a>
    <span style="color: #999; margin-left: 10px;">Total: <?= count($bukuTamu) ?> data</span>
</p>

<table>
    <thead>
        <tr>
            <th>No</th>
            <th>Nama</th>
            <th>Pesan</th>
            <th>Waktu</th>
            <th>Aksi</th>
        </tr>
    </thead>
    <tbody>
        <?php if (empty($bukuTamu)): ?>
            <tr>
                <td colspan="5" style="text-align: center; padding: 30px; color: #999;">
                    Belum ada data. <a href="tambah.php">Tambah data pertama β†’</a>
                </td>
            </tr>
        <?php else: ?>
            <?php foreach ($bukuTamu as $i => $row): ?>
            <tr>
                <td><?= $i + 1 ?></td>
                <td>
                    <strong><?= htmlspecialchars($row['nama']) ?></strong><br>
                    <small style="color:#999"><?= htmlspecialchars($row['email']) ?></small>
                </td>
                <td><?= htmlspecialchars($row['pesan']) ?></td>
                <td>
                    <small><?= date('d M Y', strtotime($row['created_at'])) ?></small><br>
                    <small style="color:#999"><?= date('H:i', strtotime($row['created_at'])) ?></small>
                </td>
                <td>
                    <div class="actions">
                        <a href="edit.php?id=<?= $row['id'] ?>" class="btn btn-warning">✏️ Edit</a>
                        <a href="hapus.php?id=<?= $row['id'] ?>" class="btn btn-danger"
                           onclick="return confirm('Yakin ingin menghapus data ini?')">πŸ—‘οΈ Hapus</a>
                    </div>
                </td>
            </tr>
            <?php endforeach; ?>
        <?php endif; ?>
    </tbody>
</table>

<?php require __DIR__ . '/../templates/footer.php'; ?>

UPDATE β€” Edit Data

File buku-tamu/edit.php:

<?php
require __DIR__ . '/../config/database.php';

$title = 'Edit Data Tamu';
$errors = [];

// Ambil ID dari URL
$id = (int) ($_GET['id'] ?? 0);

// Ambil data yang akan diedit
$stmt = $pdo->prepare("SELECT * FROM buku_tamu WHERE id = ?");
$stmt->execute([$id]);
$data = $stmt->fetch();

// Kalau data tidak ditemukan, redirect
if (!$data) {
    header("Location: /buku-tamu/");
    exit;
}

$nama = $data['nama'];
$email = $data['email'];
$pesan = $data['pesan'];

// Proses form update
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $nama = htmlspecialchars(trim($_POST['nama'] ?? ''));
    $email = filter_var(trim($_POST['email'] ?? ''), FILTER_SANITIZE_EMAIL);
    $pesan = htmlspecialchars(trim($_POST['pesan'] ?? ''));

    // Validasi
    if (empty($nama)) $errors[] = "Nama wajib diisi";
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = "Email tidak valid";
    if (empty($pesan)) $errors[] = "Pesan wajib diisi";

    // Update database
    if (empty($errors)) {
        $stmt = $pdo->prepare("UPDATE buku_tamu SET nama = ?, email = ?, pesan = ? WHERE id = ?");
        $stmt->execute([$nama, $email, $pesan, $id]);

        header("Location: /buku-tamu/?sukses=edit");
        exit;
    }
}

require __DIR__ . '/../templates/header.php';
?>

<h1>✏️ Edit Data Tamu</h1>

<?php if (!empty($errors)): ?>
    <div class="alert alert-error">
        <?php foreach ($errors as $error): ?>
            <p>⚠️ <?= $error ?></p>
        <?php endforeach; ?>
    </div>
<?php endif; ?>

<div class="form-card">
    <form method="POST">
        <div class="form-group">
            <label for="nama">Nama</label>
            <input type="text" id="nama" name="nama" value="<?= $nama ?>" required>
        </div>

        <div class="form-group">
            <label for="email">Email</label>
            <input type="email" id="email" name="email" value="<?= $email ?>" required>
        </div>

        <div class="form-group">
            <label for="pesan">Pesan</label>
            <textarea id="pesan" name="pesan" rows="5" required><?= $pesan ?></textarea>
        </div>

        <button type="submit" class="btn-submit">πŸ’Ύ Update</button>
    </form>

    <p style="margin-top: 15px;">
        <a href="/buku-tamu/">← Kembali ke daftar</a>
    </p>
</div>

<?php require __DIR__ . '/../templates/footer.php'; ?>

DELETE β€” Hapus Data

File buku-tamu/hapus.php:

<?php
require __DIR__ . '/../config/database.php';

// Ambil ID dari URL
$id = (int) ($_GET['id'] ?? 0);

if ($id > 0) {
    // Cek apakah data ada
    $stmt = $pdo->prepare("SELECT id FROM buku_tamu WHERE id = ?");
    $stmt->execute([$id]);

    if ($stmt->fetch()) {
        // Hapus data
        $stmt = $pdo->prepare("DELETE FROM buku_tamu WHERE id = ?");
        $stmt->execute([$id]);
    }
}

// Redirect kembali ke daftar
header("Location: /buku-tamu/?sukses=hapus");
exit;

Alur Kerja CRUD

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  index.php  │────▢│  tambah.php    β”‚
β”‚  (READ)     β”‚     β”‚  (CREATE)      β”‚
β”‚             β”‚     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚  Daftar     β”‚
β”‚  semua      β”‚     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  data       │────▢│  edit.php?id=  β”‚
β”‚             β”‚     β”‚  (UPDATE)      β”‚
β”‚             β”‚     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚             β”‚
β”‚             β”‚     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚             │────▢│  hapus.php?id= β”‚
β”‚             β”‚     β”‚  (DELETE)      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Tips CRUD di Dunia Nyata

Best Practices
  1. Selalu gunakan prepared statement β€” mencegah SQL Injection
  2. Validasi di server β€” meskipun sudah ada validasi JS di browser
  3. Sanitasi output β€” gunakan htmlspecialchars() saat menampilkan data
  4. Redirect setelah POST β€” cegah duplikasi data saat refresh (PRG pattern)
  5. Konfirmasi sebelum hapus β€” pakai confirm() JavaScript
  6. Soft delete β€” di production, data biasanya tidak benar-benar dihapus, hanya ditandai :::

:::warning PRG Pattern (Post-Redirect-Get) Setelah berhasil menyimpan/mengupdate data, selalu redirect ke halaman lain:

// Setelah INSERT/UPDATE berhasil:
header("Location: /buku-tamu/?sukses=edit");
exit;  // Wajib! Hentikan eksekusi setelah redirect

Kenapa? Kalau tidak redirect, pengguna yang me-refresh halaman akan mengirim form lagi (duplikasi data).

Selanjutnya

CRUD sudah lengkap! Tapi siapapun bisa mengakses halaman edit dan hapus. Kita perlu autentikasi β€” sistem login agar hanya pengguna tertentu yang bisa mengelola data. Lanjut ke Session & Autentikasi β†’.