CRUD PHP MySQLi untuk Pemula (Source Code Lengkap)
Oleh Afa HernandesDiperbarui 7 menit baca
Daftar isi
CRUD adalah singkatan dari Create, Read, Update, Delete, yaitu empat operasi dasar pada data. Hampir semua aplikasi web punya fitur CRUD, mulai dari data produk, data siswa, sampai data transaksi. Karena itu, CRUD adalah latihan pertama yang paling tepat saat belajar PHP dan MySQL.
Di tutorial ini kita membuat halaman Data Kategori dengan PHP dan MySQLi yang bisa:
- Create: menambah kategori baru
- Read: menampilkan semua kategori dalam tabel
- Update: mengubah data kategori
- Delete: menghapus kategori
Kodenya sudah memakai praktik yang aman: prepared statement untuk mencegah SQL injection, htmlspecialchars() untuk mencegah XSS, dan penghapusan lewat POST.
Catatan pembaruan: versi lama artikel ini memakai fungsi
mysql_connect()danmysql_query(). Fungsi-fungsi tersebut sudah dihapus sejak PHP 7, sehingga tidak bisa dijalankan di XAMPP versi sekarang. Kode di bawah sudah diperbarui dan diuji di PHP 8.
Persiapan
- XAMPP (atau Laragon) yang berisi PHP 8 dan MySQL/MariaDB. Panduannya ada di Cara Install dan Menggunakan XAMPP.
- Jalankan Apache dan MySQL dari XAMPP Control Panel.
- Buat folder
crud-kategoridi dalamC:\xampp\htdocs\.
Struktur file yang akan kita buat:
htdocs/crud-kategori/├── database.sql ← struktur tabel + data contoh├── koneksi.php ← koneksi database & fungsi bantu└── kategori.php ← halaman CRUDLangkah 1: Buat database dan tabel
Buka phpMyAdmin (http://localhost/phpmyadmin), pilih tab SQL, lalu jalankan:
CREATE DATABASE IF NOT EXISTS db_ecommerce CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;USE db_ecommerce;
CREATE TABLE IF NOT EXISTS tb_kategori ( kode_kategori VARCHAR(10) NOT NULL PRIMARY KEY, nama_kategori VARCHAR(100) NOT NULL, keterangan VARCHAR(255) NULL);
INSERT INTO tb_kategori (kode_kategori, nama_kategori, keterangan) VALUES('KTG001', 'Pakaian', 'Baju, celana, dan jaket'),('KTG002', 'Elektronik', 'Gadget dan aksesoris');kode_kategori dijadikan PRIMARY KEY, sehingga tidak boleh ada dua kategori dengan kode yang sama. Nanti kita menangani kasus ini dengan pesan yang ramah.
Langkah 2: Buat koneksi database
<?php// Tampilkan error database sebagai exception agar mudah dilacakmysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$conn = new mysqli('localhost', 'root', '', 'db_ecommerce');$conn->set_charset('utf8mb4');
// Amankan teks sebelum ditampilkan di HTML (mencegah XSS)function e(?string $teks): string{ return htmlspecialchars($teks ?? '', ENT_QUOTES, 'UTF-8');}Parameter new mysqli() berurutan: host, username, password, dan nama database. Di XAMPP, user default-nya root tanpa password.
Fungsi e() kita pakai setiap kali menampilkan data ke HTML. Jika ada yang mengisi nama kategori dengan <script>…</script>, teks itu tampil apa adanya dan tidak dijalankan sebagai kode.
Langkah 3: Buat halaman CRUD
Semua operasi ada di satu file. Bagian PHP di atas memproses data, dan bagian HTML di bawah menampilkan form serta tabel.
<?phprequire 'koneksi.php';
$pesan = '';$form = ['kode_kategori' => '', 'nama_kategori' => '', 'keterangan' => ''];$kodeLama = ''; // terisi saat mode ubah
// ---------- CREATE & UPDATE ----------if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['simpan'])) { $form = [ 'kode_kategori' => trim($_POST['kode_kategori'] ?? ''), 'nama_kategori' => trim($_POST['nama_kategori'] ?? ''), 'keterangan' => trim($_POST['keterangan'] ?? ''), ]; $kodeLama = $_POST['kode_lama'] ?? '';
if ($form['kode_kategori'] === '' || $form['nama_kategori'] === '') { $pesan = 'Kode dan nama kategori wajib diisi.'; } else { try { if ($kodeLama === '') { $stmt = $conn->prepare( 'INSERT INTO tb_kategori (kode_kategori, nama_kategori, keterangan) VALUES (?, ?, ?)' ); $stmt->bind_param('sss', $form['kode_kategori'], $form['nama_kategori'], $form['keterangan']); } else { $stmt = $conn->prepare( 'UPDATE tb_kategori SET kode_kategori = ?, nama_kategori = ?, keterangan = ? WHERE kode_kategori = ?' ); $stmt->bind_param('ssss', $form['kode_kategori'], $form['nama_kategori'], $form['keterangan'], $kodeLama); } $stmt->execute(); header('Location: kategori.php?status=tersimpan'); exit; } catch (mysqli_sql_exception $ex) { // 1062 = duplicate entry (kode sudah dipakai) $pesan = $ex->getCode() === 1062 ? "Kode {$form['kode_kategori']} sudah dipakai." : 'Gagal menyimpan data.'; } }}
// ---------- DELETE (lewat POST, bukan link GET) ----------if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['hapus'])) { $kode = $_POST['hapus']; $stmt = $conn->prepare('DELETE FROM tb_kategori WHERE kode_kategori = ?'); $stmt->bind_param('s', $kode); $stmt->execute(); header('Location: kategori.php?status=terhapus'); exit;}
// ---------- READ satu data untuk diubah ----------if (isset($_GET['ubah'])) { $kode = $_GET['ubah']; $stmt = $conn->prepare('SELECT * FROM tb_kategori WHERE kode_kategori = ?'); $stmt->bind_param('s', $kode); $stmt->execute(); $data = $stmt->get_result()->fetch_assoc(); if ($data) { $form = $data; $kodeLama = $data['kode_kategori']; }}
// ---------- READ semua data ----------$daftar = $conn->query('SELECT * FROM tb_kategori ORDER BY kode_kategori')->fetch_all(MYSQLI_ASSOC);
$status = [ 'tersimpan' => 'Data berhasil disimpan.', 'terhapus' => 'Data berhasil dihapus.',][$_GET['status'] ?? ''] ?? '';?><!DOCTYPE html><html lang="id"><head> <meta charset="UTF-8"> <title>Data Kategori</title> <style> body { font-family: sans-serif; max-width: 760px; margin: 2rem auto; padding: 0 1rem; } table { width: 100%; border-collapse: collapse; margin-top: 1rem; } th, td { border: 1px solid #ccc; padding: .5rem; text-align: left; } th { background: #f2f2f2; } label { display: block; margin-top: .75rem; } input[type=text] { width: 100%; padding: .4rem; } .info { background: #e7f6ec; padding: .5rem; } .error { background: #fde8e8; padding: .5rem; } form.inline { display: inline; } </style></head><body> <h1>Data Kategori</h1>
<?php if ($status): ?><p class="info"><?= e($status) ?></p><?php endif; ?> <?php if ($pesan): ?><p class="error"><?= e($pesan) ?></p><?php endif; ?>
<form method="post"> <input type="hidden" name="kode_lama" value="<?= e($kodeLama) ?>"> <label>Kode kategori <input type="text" name="kode_kategori" maxlength="10" value="<?= e($form['kode_kategori']) ?>" required> </label> <label>Nama kategori <input type="text" name="nama_kategori" maxlength="100" value="<?= e($form['nama_kategori']) ?>" required> </label> <label>Keterangan <input type="text" name="keterangan" maxlength="255" value="<?= e($form['keterangan']) ?>"> </label> <p> <button type="submit" name="simpan"><?= $kodeLama ? 'Simpan Perubahan' : 'Tambah' ?></button> <?php if ($kodeLama): ?><a href="kategori.php">Batal</a><?php endif; ?> </p> </form>
<table> <tr><th>No</th><th>Kode</th><th>Nama</th><th>Keterangan</th><th>Aksi</th></tr> <?php foreach ($daftar as $i => $row): ?> <tr> <td><?= $i + 1 ?></td> <td><?= e($row['kode_kategori']) ?></td> <td><?= e($row['nama_kategori']) ?></td> <td><?= e($row['keterangan']) ?></td> <td> <a href="?ubah=<?= urlencode($row['kode_kategori']) ?>">Ubah</a> | <form class="inline" method="post" onsubmit="return confirm('Hapus data ini?')"> <button type="submit" name="hapus" value="<?= e($row['kode_kategori']) ?>">Hapus</button> </form> </td> </tr> <?php endforeach; ?> </table> <p>Jumlah data: <?= count($daftar) ?> kategori</p></body></html>Buka http://localhost/crud-kategori/kategori.php di browser. Anda akan melihat form di atas dan tabel data di bawahnya:

Penjelasan alur CRUD
Create & Update memakai form yang sama. Field tersembunyi kode_lama menentukan mode: kosong berarti tambah (INSERT), terisi berarti ubah (UPDATE … WHERE kode_kategori = kode_lama). Dengan begitu, kode kategori tetap bisa diganti saat diubah.
Read dilakukan dua kali: satu query untuk mengisi tabel, dan satu query untuk mengambil data yang diklik Ubah agar form terisi otomatis.
Delete memakai tombol di dalam <form method="post"> dengan konfirmasi JavaScript. Penghapusan tidak memakai link GET seperti ?hapus=KTG001, karena link bisa terpicu tanpa sengaja, misalnya oleh crawler atau saat pratinjau link di aplikasi chat.
Redirect setelah POST (header('Location: …')) mencegah data terkirim dua kali saat pengguna menekan refresh. Pola ini disebut Post/Redirect/Get.
Mengapa prepared statement penting
Perhatikan perbedaan dua cara menulis query berikut:
// ❌ RENTAN: input pengguna langsung masuk ke query$sql = "DELETE FROM tb_kategori WHERE kode_kategori = '{$_POST['hapus']}'";
// ✅ AMAN: input dikirim terpisah dari query$stmt = $conn->prepare('DELETE FROM tb_kategori WHERE kode_kategori = ?');$stmt->bind_param('s', $kode);$stmt->execute();Pada cara pertama, jika seseorang mengirim nilai x' OR '1'='1, query berubah menjadi … WHERE kode_kategori = 'x' OR '1'='1', dan seluruh data terhapus. Ini yang disebut SQL injection. Dengan prepared statement, nilai tersebut hanya dianggap teks biasa, sehingga tidak ada data yang terhapus. Kode di tutorial ini sudah kami uji dengan input semacam itu.
Huruf di bind_param('sss', …) menunjukkan tipe tiap parameter: s untuk string, i untuk integer, d untuk desimal.
Bonus: combo box dari database
Kebutuhan yang sering muncul adalah drop-down yang isinya diambil dari tabel lain, misalnya memilih kategori saat menambah produk. Saat mode ubah, opsi yang tersimpan otomatis terpilih:
<?php$kategori = $conn->query('SELECT kode_kategori, nama_kategori FROM tb_kategori ORDER BY nama_kategori') ->fetch_all(MYSQLI_ASSOC);$terpilih = $produk['kode_kategori'] ?? ''; // nilai tersimpan saat mode ubah?><select name="kode_kategori" required> <option value="">-- Pilih kategori --</option> <?php foreach ($kategori as $k): ?> <option value="<?= e($k['kode_kategori']) ?>" <?= $k['kode_kategori'] === $terpilih ? 'selected' : '' ?>> <?= e($k['nama_kategori']) ?> </option> <?php endforeach; ?></select>Error yang sering muncul
Fatal error: Uncaught Error: Call to undefined function mysql_connect(). Kode memakai fungsi mysql_* lama yang sudah dihapus di PHP 7. Ganti dengan MySQLi atau PDO seperti contoh di atas.
mysqli_sql_exception: Unknown database 'db_ecommerce'. Database belum dibuat. Jalankan isi database.sql di phpMyAdmin.
mysqli_sql_exception: No connection could be made because the target machine actively refused it. MySQL belum berjalan. Klik Start pada modul MySQL di XAMPP Control Panel.
Access denied for user 'root'@'localhost'. Password user root di komputer Anda tidak kosong. Sesuaikan argumen ketiga new mysqli().
Cannot modify header information - headers already sent. Ada output (spasi, baris kosong, atau echo) sebelum header(). Pastikan tidak ada karakter apa pun sebelum <?php di koneksi.php dan kategori.php.
Pertanyaan yang sering diajukan
Lebih baik MySQLi atau PDO? Keduanya sama-sama aman jika memakai prepared statement. MySQLi khusus untuk MySQL/MariaDB, sedangkan PDO mendukung banyak database (MySQL, PostgreSQL, SQLite) dengan kode yang sama. Untuk belajar, MySQLi sudah cukup. Untuk project yang mungkin berganti database, pilih PDO.
Bagaimana jika ingin tampilan yang lebih bagus? Tambahkan framework CSS seperti Bootstrap atau Tailwind tanpa mengubah logika PHP-nya. Untuk aplikasi yang lebih besar, gunakan framework PHP seperti Laravel atau CakePHP, yang sudah menyediakan fitur CRUD, validasi, dan keamanan bawaan.
Bagaimana cara mencetak atau mengekspor data? Lihat Cara Membuat Print Preview PHP & MySQL dan Import File Excel ke MySQL dengan PHP.
Kesimpulan
CRUD PHP MySQLi cukup dibangun dengan tiga file: database.sql, koneksi.php, dan kategori.php. Kuncinya ada di prepared statement untuk setiap query yang memakai input pengguna, htmlspecialchars() saat menampilkan data, dan POST + redirect untuk operasi yang mengubah data. Dengan pola ini, aplikasi Anda sudah jauh lebih aman daripada kebanyakan contoh CRUD di internet.

