Belajar CRUD PHP MySQL dari 0 Sampai Online: Login Multi-Role + Konsep IPO

jujur aja, momen paling nagih waktu belajar PHP itu pas website kita akhirnya bisa dibuka dari HP teman. bukan cuma echo "halo" di localhost, tapi beneran ada yang bisa daftar, login, datanya kesimpan, adminnya bisa atur-atur. nah di panduan ini kita bikin itu: aplikasi CRUD user lengkap — register, login multi-role admin dan user, tambah, lihat, edit, hapus, logout — terus kita online-kan ke hosting.
kalau kalian sudah baca membuat website dari nol sampai online yang bahas form OSIS 24 input, artikel ini adalah sodaranya yang lebih fokus ke fondasi: alur login + hak akses + keamanan dasar. source code-nya saya ambil dari demo belajar-web, tapi saya bedah ulang per baris dengan bahasa santai. tiap baris saya kasih alasannya: kenapa pakai fungsi ini, kenapa tidak pakai yang itu.
Daftar Isi
- 1. Konsep Dasar: IPO + Website Dinamis
- 2. Persiapan: XAMPP + VS Code + Folder Project
- 3. Desain Database: Satu Tabel Users
- 4. config.php: Koneksi + Session + Satpam
- 5. index.php: Beranda yang Tahu Kamu Siapa
- 6. register.php: Daftar Akun Baru
- 7. login.php: Masuk + Dikasih Gelang Session
- 8. dashboard.php: Read + Bedakan Admin dan User
- 9. tambah.php: Create Khusus Admin
- 10. edit.php: Update + Siapa Boleh Ubah Siapa
- 11. hapus.php + logout.php: Delete + Keluar
- 12. Deploy dari Localhost Sampai Online
- 13. Error Paling Sering + Best Practice
- 14. Kesimpulan
1. Konsep Dasar: IPO + Website Dinamis
kalau website statis itu kayak brosur: isinya itu-itu aja. website dinamis itu kayak warteg: menunya sama, tapi isinya beda-beda tergantung siapa yang pesan. bedanya ada database di belakangnya.
di semua file demo ini saya kasih komentar 1. INPUT - 2. PROSES - 3. OUTPUT, atau disingkat IPO. ini bukan teori ngawang, tapi beneran kepakai tiap halaman:
flowchart LR
A["Browser<br/>form + link + tabel"] -- "POST / GET / klik" --> B["PHP<br/>validasi + query<br/>mysqli prepare"]
B -- "simpan / baca" --> C[("MySQL<br/>tabel users")]
B -- "redirect / HTML" --> A
B -- "ingat login" --> D["Session<br/>user_id + role"]
style A fill:#eef4ff,stroke:#000,stroke-width:3px
style B fill:#f0eefc,stroke:#000,stroke-width:3px
style C fill:#e6f7fb,stroke:#000,stroke-width:3px
style D fill:#fff7e6,stroke:#000,stroke-width:3px
- Input: datanya datang dari
$_POST(form),$_GET(URL kayakedit.php?id=5), atau$_SESSION(ingatan login). - Proses: PHP ngecek: kosong nggak? emailnya valid nggak? passwordnya cocok nggak? terus ngomong ke MySQL pakai prepared statement.
- Output: kalau sukses, tendang pakai
header("Location: ..."). kalau gagal atau baru buka, tampilin HTML lagi plus pesan error.
kenapa IPO penting? biar kalian tidak nyasar. tiap kali bingung “ini kode ngapain”, tinggal tanya: ini inputnya dari mana, diproses gimana, outputnya ke mana. pola yang sama dipakai juga di artikel membuat CRUD PHP mysqli, cuma di sini ditambah login multi-role.
satu catatan jujur soal naskah video H4: di video, biar durasinya muat 30 menit, query loginnya ditulis versi pendek pakai string langsung. di source code final belajar-web yang kita bedah di sini, semuanya sudah saya upgrade ke prepared statement + password_hash. alasannya simpel: versi pendek gampang dibobol, versi final aman. jadi kalau kalian lihat bedanya, itu disengaja.
2. Persiapan: XAMPP + VS Code + Folder Project
kalian butuh server lokal, bukan Live Server lagi. Live Server cuma paham HTML, tidak paham PHP.
- Instal XAMPP (atau Laragon kalau mau yang lebih enteng) — centang Apache + MySQL. cara pakai XAMPP-nya sama kayak di panduan OSIS.
- Instal VS Code + ekstensi
PHP Intelephensebiar ada auto-complete. - Nyalakan Apache + MySQL di XAMPP Control Panel, buka
http://localhostbuat mastiin nyala. - Bikin folder
C:\xampp\htdocs\belajar-web(kalau pakai Laragon:C:\laragon\www\belajar-web), buka di VS Code. - Bikin database
belajar_crud— caranya di bab berikut, tinggal import satu file.
struktur akhirnya kecil aja, 10 file:
belajar-web/
├── config.php // koneksi + session + fungsi satpam
├── database.sql // struktur tabel + 2 akun contoh
├── index.php // beranda, beda tampilan kalau sudah login
├── register.php // daftar akun baru (otomatis jadi user)
├── login.php // masuk + isi session
├── dashboard.php // lihat data (admin: semua, user: sendiri)
├── tambah.php // tambah user baru (admin only)
├── edit.php // ubah data (admin: semua, user: sendiri)
├── hapus.php // hapus user (admin only, tidak boleh hapus diri)
└── logout.php // keluar + hancurkan session
tidak ada CSS sama sekali di demo ini, dan itu disengaja. biar mata kalian fokus ke alur PHP-nya dulu, bukan ke warna tombol. nanti kalau alurnya sudah paham, dandanin tampilannya gampang.
3. Desain Database: Satu Tabel Users
buka phpMyAdmin (http://localhost/phpmyadmin), tab Import, masukkan file database.sql ini. atau copy-paste ke tab SQL:
CREATE DATABASE IF NOT EXISTS belajar_crud
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;
USE belajar_crud;
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
role ENUM('admin','user') NOT NULL DEFAULT 'user',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- Password admin: admin123
INSERT INTO users (nama, email, password, role) VALUES
('Administrator', '[email protected]', '$2y$10$6nJg8fV7BwSjNK75xwz4POGX0mrwv6IzHtqv0Siju0uKcS.tEJwJ.', 'admin'),
('Budi Santoso', '[email protected]', '$2y$10$ywX3wS4NcUYwG64I.iHaNO9kRCHYn0DeN972ehQQyWCXvw/JhnhNa', 'user')
ON DUPLICATE KEY UPDATE nama = VALUES(nama);
Penjelasan Detail Per Baris database.sql
bayangin database itu lemari, tabel itu laci, kolom itu sekatnya.
| Baris | Kode | Artinya buat pemula + alasannya |
|---|---|---|
| 1 | CREATE DATABASE IF NOT EXISTS belajar_crud ... utf8mb4 | bikin lemari baru. IF NOT EXISTS biar tidak error kalau di-import dua kali. utf8mb4 biar nama orang Indonesia plus emoji kesimpan bener, bukan jadi ???. |
| 2 | USE belajar_crud; | buka lemarinya. semua perintah di bawah dikerjain di dalam lemari ini, bukan lemari lain. |
| 3 | id INT AUTO_INCREMENT PRIMARY KEY | nomor urut otomatis 1, 2, 3. PRIMARY KEY itu KTP-nya baris: unik, tidak boleh kembar, dipakai buat edit.php?id=... dan relasi. |
| 4 | nama VARCHAR(100) NOT NULL | teks pendek max 100 huruf, wajib diisi. kenapa 100 bukan 255? biar hemat dan realistis, nama orang jarang lebih dari 100. |
| 5 | email VARCHAR(100) NOT NULL UNIQUE | UNIQUE itu kuncinya: tidak boleh ada dua akun dengan email sama. kalau tidak ada ini, orang bisa daftar dua kali pakai email yang sama dan login jadi ambigu. |
| 6 | password VARCHAR(255) NOT NULL | sengaja 255 karena isinya bukan password asli, tapi hash hasil password_hash yang panjangnya sekitar 60 karakter. kalau kolomnya kependekan, hash-nya kepotong dan login gagal terus. |
| 7 | role ENUM('admin','user') NOT NULL DEFAULT 'user' | pilihan dikunci cuma boleh admin atau user. DEFAULT 'user' biar pendaftar baru otomatis jadi user biasa, tidak bisa nyusup jadi admin. ibarat stempel default “tamu”. |
| 8 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | stempel waktu otomatis. kalian tidak perlu isi manual, MySQL yang ngisi jam sekarang. kepakai buat urutan di dashboard. |
| 9 | INSERT INTO users ... ON DUPLICATE KEY UPDATE | masukin 2 akun contoh biar langsung bisa dicoba: [email protected] / admin123 dan [email protected] / user123. ON DUPLICATE KEY UPDATE biar aman di-import ulang tanpa error duplikat. |
akun contohnya jangan dihapus dulu ya, kepakai buat ngetes bedanya admin vs user nanti.
4. config.php: Koneksi + Session + Satpam
file ini di-require di semua halaman. ibarat saklar listrik utama: kalau file ini rusak, semua halaman mati.
<?php
// Memulai session PHP agar variabel $_SESSION bisa digunakan di seluruh halaman
session_start();
// Pengaturan koneksi database MySQL
$DB_HOST = 'localhost';
$DB_USER = 'root';
$DB_PASS = '';
$DB_NAME = 'belajar_crud';
// Membuat koneksi ke database
$conn = mysqli_connect($DB_HOST, $DB_USER, $DB_PASS, $DB_NAME);
// Cek apakah koneksi berhasil atau gagal
if (!$conn) {
die('Koneksi ke database gagal: ' . mysqli_connect_error());
}
// Mengatur charset UTF-8
mysqli_set_charset($conn, 'utf8mb4');
// Fungsi pembantu: Cek apakah user sudah login
function sudahLogin() {
return isset($_SESSION['user_id']);
}
// Fungsi pembantu: Wajibkan login (jika belum, arahkan ke login.php)
function wajibLogin() {
if (!sudahLogin()) {
header('Location: login.php');
exit;
}
}
// Fungsi pembantu: Wajibkan role admin (jika bukan admin, hentikan proses)
function wajibAdmin() {
wajibLogin();
if ($_SESSION['role'] !== 'admin') {
die('Akses ditolak: halaman ini khusus untuk pengguna dengan peran Admin.');
}
}
Penjelasan Detail Per Baris config.php
| Baris | Kode | Artinya + alasannya |
|---|---|---|
| 1 | session_start(); | nyalain ingatan PHP. wajib di baris paling atas sebelum HTML apa pun. tanpa ini $_SESSION tidak jalan dan login tidak akan pernah “nempel”. |
| 2 | $DB_HOST = 'localhost'; dst | data alamat database. di lokal root tanpa password itu default XAMPP. nanti di hosting diganti sesuai dashboard hosting, bukan localhost lagi. makanya ditaruh di satu file biar gantinya sekali aja. |
| 3 | $conn = mysqli_connect(...) | buka sambungan ke MySQL. hasilnya disimpan di $conn dan dipakai semua file lain. kenapa prosedural mysqli_connect bukan new mysqli? biar konsisten satu gaya di semua file demo, pemula tidak bingung lihat dua gaya. |
| 4 | if (!$conn) die(...) | kalau alamatnya salah, langsung berhenti + kasih tahu penyebabnya. jangan dilanjutin dengan sambungan rusak, nanti error-nya misterius di file lain. |
| 5 | mysqli_set_charset($conn, 'utf8mb4') | samain bahasa laptop dan database. kalau lupa, nama kayak Siti ❤️ bisa kesimpan jadi simbol aneh. |
| 6 | function sudahLogin() | nanya satu hal doang: ada user_id di session nggak? isset = sudah login, tidak ada = tamu. dibungkus fungsi biar tidak nulis isset($_SESSION['user_id']) berulang-ulang. |
| 7 | function wajibLogin() | satpam pintu. kalau belum login, tendang ke login.php + exit biar kode di bawah tidak ikut jalan. exit itu wajib, kalau lupa user tetap bisa lihat halaman walau sudah di-redirect. |
| 8 | function wajibAdmin() | satpam lapis dua. pertama cek sudah login belum, terus cek role-nya admin bukan. kenapa pakai die bukan redirect? biar jelas ini pelanggaran akses, bukan sekadar belum login. |
5. index.php: Beranda yang Tahu Kamu Siapa
halaman paling sederhana. tidak ada query simpan, cuma nanya “eh kamu sudah login belum?”.
<?php
// Memanggil konfigurasi & session
require_once 'config.php';
// Memeriksa status login dari session
$login = sudahLogin();
$nama = $login ? ($_SESSION['nama'] ?? '') : '';
$role = $login ? ($_SESSION['role'] ?? '') : '';
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>Aplikasi CRUD User - Bahan Belajar PHP</title>
</head>
<body>
<h1>Aplikasi CRUD Sederhana (PHP & MySQL)</h1>
<p>Contoh bahan ajar konsep <strong>IPO (Input - Proses - Output)</strong> untuk PEMULA.</p>
<hr>
<?php if ($login): ?>
<p>Halo, <strong><?= htmlspecialchars($nama) ?></strong> (Role: <?= htmlspecialchars($role) ?>)</p>
<p>
<a href="dashboard.php">[ Masuk ke Dashboard ]</a>
<a href="logout.php">[ Logout ]</a>
</p>
<?php else: ?>
<p>Silakan masuk atau buat akun baru:</p>
<p>
<a href="login.php">[ Halaman Login ]</a> |
<a href="register.php">[ Halaman Register ]</a>
</p>
<?php endif; ?>
<hr>
<h3>Akun Uji Coba:</h3>
<ul>
<li><strong>Admin:</strong> [email protected] / admin123 (bisa kelola semua data)</li>
<li><strong>User:</strong> [email protected] / user123 (hanya bisa edit data diri)</li>
</ul>
</body>
</html>
Penjelasan Detail Per Baris index.php
| Baris | Kode | Artinya + alasannya |
|---|---|---|
| 1 | require_once 'config.php'; | colok listrik + nyalain session. pakai require_once (bukan include) biar kalau file hilang langsung error keras, bukan halaman setengah mati. _once biar tidak ke-load dua kali. |
| 2 | $login = sudahLogin(); | Input-nya cuma satu: status session. tidak ada form di sini, jadi prosesnya ya cuma ngecek ini. |
| 3 | $nama = $login ? ($_SESSION['nama'] ?? '') : ''; | kalau sudah login ambil nama dari session, kalau belum kosongin. ?? '' itu gaya PHP 8 (null coalescing) yang dibahas di video H4: pengganti isset panjang. alasannya biar tidak muncul warning undefined index kalau session-nya kepotong. |
| 4 | <?php if ($login): ?> ... <?php else: ?> | Output bercabang: yang sudah login dikasih jalan ke dashboard, tamu dikasih jalan ke login/register. satu file, dua wajah. |
| 5 | htmlspecialchars($nama) | wajib tiap nampilin data dari user/session ke HTML. fungsinya ngubah <script> jadi teks biasa biar tidak bisa nyelipin JavaScript jahat (XSS). |
| 6 | info akun uji coba di bawah | disengaja ditampilin biar kalian langsung bisa coba tanpa intip database. di project asli jangan lakukan ini ya, ini cuma buat bahan belajar. |
6. register.php: Daftar Akun Baru
pola IPO paling lengkap pertama: form → validasi → cek duplikat → hash → simpan → tendang ke login.
<?php
require_once 'config.php';
// Jika sudah login, langsung alihkan ke dashboard
if (sudahLogin()) {
header('Location: dashboard.php');
exit;
}
$error = '';
// 1. INPUT (Menerima input pendaftaran dari form POST)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$nama = trim($_POST['nama'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$konfirmasi = $_POST['konfirmasi'] ?? '';
// 2. PROSES (Validasi Input, Cek Duplikasi, Hash, Simpan DB)
if ($nama === '' || $email === '' || $password === '') {
$error = 'Nama, email, dan password wajib diisi!';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$error = 'Format penulisan email tidak valid!';
} elseif (strlen($password) < 6) {
$error = 'Password minimal harus 6 karakter!';
} elseif ($password !== $konfirmasi) {
$error = 'Konfirmasi password tidak cocok!';
} else {
// Cek apakah email sudah terdaftar sebelumnya
$stmt = mysqli_prepare($conn, 'SELECT id FROM users WHERE email = ? LIMIT 1');
mysqli_stmt_bind_param($stmt, 's', $email);
mysqli_stmt_execute($stmt);
mysqli_stmt_store_result($stmt);
$sudahAda = mysqli_stmt_num_rows($stmt) > 0;
mysqli_stmt_close($stmt);
if ($sudahAda) {
$error = 'Email sudah digunakan. Silakan gunakan email lain atau langsung login.';
} else {
$hash = password_hash($password, PASSWORD_DEFAULT);
$role = 'user';
$stmt = mysqli_prepare($conn, 'INSERT INTO users (nama, email, password, role) VALUES (?, ?, ?, ?)');
mysqli_stmt_bind_param($stmt, 'ssss', $nama, $email, $hash, $role);
if (mysqli_stmt_execute($stmt)) {
mysqli_stmt_close($stmt);
// 3. OUTPUT (Jika Berhasil: Redirect ke halaman Login)
header('Location: login.php?daftar=sukses');
exit;
} else {
$error = 'Gagal menyimpan ke database, silakan coba lagi.';
mysqli_stmt_close($stmt);
}
}
}
}
?>
form HTML-nya (potongan penting):
<form method="post" action="register.php">
<input type="text" name="nama" id="nama" required value="<?= htmlspecialchars($_POST['nama'] ?? '') ?>">
<input type="email" name="email" id="email" required value="<?= htmlspecialchars($_POST['email'] ?? '') ?>">
<input type="password" name="password" id="password" required>
<input type="password" name="konfirmasi" id="konfirmasi" required>
<button type="submit">Daftar Sekarang</button>
</form>
Penjelasan Detail Per Baris register.php
| Blok | Kode | Artinya + alasannya |
|---|---|---|
| satpam atas | if (sudahLogin()) header('Location: dashboard.php') | orang yang sudah login ngapain daftar lagi? langsung tendang ke dashboard. ini mencegah akun ganda gara-gara iseng. |
| input | $_SERVER['REQUEST_METHOD'] === 'POST' | ngecek “apakah user baru aja klik Daftar?”. kalau baru buka halaman (GET), jangan validasi apa-apa, langsung tampilin form kosong. |
| input | trim($_POST['nama'] ?? '') | trim = gunting spasi depan-belakang " Budi " jadi "Budi". ?? '' = kalau tidak dikirim, anggap kosong, jangan error. dua-duanya gaya PHP 8 yang bersih. |
| input | $_POST['password'] tanpa trim | password sengaja tidak di-trim. alasannya: spasi bisa jadi bagian password yang sah. kalau di-trim, password " rahasia " berubah tanpa sepengetahuan user. |
| validasi | cek kosong dulu | urutannya disengaja dari yang paling murah (cek kosong) ke yang paling mahal (query database). ngapain nanya database kalau emailnya aja kosong? |
| validasi | filter_var($email, FILTER_VALIDATE_EMAIL) | polisi email bawaan PHP. budi@ tanpa belakang langsung ditolak jadi false. jangan bikin regex sendiri, yang bawaan sudah teruji. |
| validasi | strlen($password) < 6 | batas minimal 6 biar tidak gampang ditebak. kenapa 6 bukan 8? kompromi buat belajar: cukup aman, tidak bikin user malas daftar. di project asli naikkan ke 8. |
| validasi | $password !== $konfirmasi | jebakan typo. user disuruh ketik dua kali biar ketahuan kalau jarinya keseleo. pakai !== (strict) biar tipe datanya juga dibandingin. |
| cek duplikat | SELECT id FROM users WHERE email = ? LIMIT 1 | nanya database: email ini sudah ada belum? LIMIT 1 biar berhenti di ketemu pertama, tidak usah scan semua. pakai ? + bind_param biar aman dari SQL injection. |
| hash | password_hash($password, PASSWORD_DEFAULT) | ubah password jadi kode acak satu arah. yang disimpan ke database hash-nya, bukan password aslinya. PASSWORD_DEFAULT biar algoritmanya ngikutin yang terbaru, tidak usah mikir bcrypt atau argon2 manual. |
| role | $role = 'user'; hardcode | ini penting: pendaftar mandiri selalu jadi user. tidak ada input role di form register. kalau role bisa dipilih dari form, semua orang bakal daftar jadi admin. mau bikin admin baru? lewat tambah.php oleh admin. |
| simpan | INSERT INTO users ... VALUES (?, ?, ?, ?) + bind_param('ssss', ...) | ? itu lubang yang nanti diisi data. 'ssss' artinya keempatnya string. kenapa repot-repot? biar data dan perintah SQL kepisah. hacker kirim email = "x' OR '1'='1" pun dianggap teks biasa, bukan perintah. |
| output sukses | header('Location: login.php?daftar=sukses') | habis daftar langsung tendang ke login dengan pesan ?daftar=sukses. ini pola redirect after POST: biar kalau user refresh, form tidak kekirim dua kali. |
| output gagal | $error = '...' lalu tampil di HTML | kalau gagal, jangan redirect. tampilin lagi formnya + pesan merahnya. makanya $error di-htmlspecialchars pas tampil biar aman. |
| form | value="<?= htmlspecialchars($_POST['nama'] ?? '') ?>" | biar user tidak ngetik ulang dari nol kalau ada yang salah. data lama ditampilin lagi. password sengaja tidak diisi ulang demi keamanan. |
7. login.php: Masuk + Dikasih Gelang Session
mirip register, tapi prosesnya kebalik: bukan nyimpan, melainkan nyari + nyocokin.
<?php
require_once 'config.php';
if (sudahLogin()) {
header('Location: dashboard.php');
exit;
}
$error = '';
// 1. INPUT
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
// 2. PROSES
if ($email === '' || $password === '') {
$error = 'Email dan password tidak boleh kosong!';
} else {
$stmt = mysqli_prepare($conn, 'SELECT id, nama, email, password, role FROM users WHERE email = ? LIMIT 1');
mysqli_stmt_bind_param($stmt, 's', $email);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$user = mysqli_fetch_assoc($result);
mysqli_stmt_close($stmt);
if ($user && password_verify($password, $user['password'])) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['nama'] = $user['nama'];
$_SESSION['email'] = $user['email'];
$_SESSION['role'] = $user['role'];
// 3. OUTPUT sukses
header('Location: dashboard.php');
exit;
} else {
$error = 'Email atau password salah!';
}
}
}
?>
Penjelasan Detail Per Baris login.php
| Baris | Kode | Artinya + alasannya |
|---|---|---|
| 1 | SELECT ... WHERE email = ? LIMIT 1 | cari user by email, bukan by nama. alasannya email itu UNIQUE, namanya bisa kembar. LIMIT 1 karena email unik, ketemu satu langsung berhenti. |
| 2 | mysqli_stmt_get_result + mysqli_fetch_assoc | ambil hasilnya jadi array: ['id'=>1, 'password'=>'$2y$10$...', ...]. kalau email tidak ada, $user jadi null. |
| 3 | if ($user && password_verify(...)) | dua syarat: user-nya ada dan passwordnya cocok. password_verify("admin123", "$2y$10$...") itu cara benernya. jangan pernah bandingin hash pakai ==, hash tidak bisa dibaca balik. |
| 4 | $_SESSION['user_id'] = ... dst | ini momen “kasih gelang”: kalau cocok, tempel id, nama, email, role ke session. mulai detik ini semua halaman tahu kamu siapa tanpa perlu login ulang tiap klik. |
| 5 | pesan error-nya sengaja ambigu | Email atau password salah! — tidak dikasih tahu yang salah emailnya atau passwordnya. alasannya keamanan: kalau dibilang “email tidak terdaftar”, hacker jadi tahu email mana yang beneran ada. |
| 6 | login.php?daftar=sukses dibaca di HTML | habis register, user mendarat di sini lihat pesan hijau “Pendaftaran berhasil!”. kecil tapi penting buat UX: user tahu dia harus ngapain selanjutnya. |
8. dashboard.php: Read + Bedakan Admin dan User
ini huruf R di CRUD: Read. plus ini panggung utama bedain admin vs user.
<?php
require_once 'config.php';
wajibLogin();
// 1. INPUT
$userId = (int)$_SESSION['user_id'];
$namaUser = $_SESSION['nama'];
$roleUser = $_SESSION['role'];
$isAdmin = ($roleUser === 'admin');
$pesanNotifikasi = $_GET['pesan'] ?? '';
// 2. PROSES
if ($isAdmin) {
$query = mysqli_query($conn, 'SELECT id, nama, email, role, created_at FROM users ORDER BY id ASC');
$users = mysqli_fetch_all($query, MYSQLI_ASSOC);
} else {
$stmt = mysqli_prepare($conn, 'SELECT id, nama, email, role, created_at FROM users WHERE id = ? LIMIT 1');
mysqli_stmt_bind_param($stmt, 'i', $userId);
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
$users = [mysqli_fetch_assoc($res)];
mysqli_stmt_close($stmt);
}
?>
tabel HTML-nya intinya gini:
<table border="1" cellpadding="8" cellspacing="0" width="100%">
<!-- header: No | Nama | Email | Role | Dibuat | Aksi -->
<?php $no = 1; foreach ($users as $u): ?>
<tr>
<td><?= $no++ ?></td>
<td><?= htmlspecialchars($u['nama']) ?></td>
<td><?= htmlspecialchars($u['email']) ?></td>
<td><?= htmlspecialchars($u['role']) ?></td>
<td><?= htmlspecialchars($u['created_at']) ?></td>
<td>
<a href="edit.php?id=<?= (int)$u['id'] ?>">Edit</a>
<!-- Hapus cuma muncul kalau admin + bukan akun sendiri -->
</td>
</tr>
<?php endforeach; ?>
</table>
Penjelasan Detail Per Baris dashboard.php
| Baris | Kode | Artinya + alasannya |
|---|---|---|
| 1 | wajibLogin(); di paling atas | satpam pintu. belum login langsung ditendang sebelum query apa pun jalan. alasannya biar data user tidak bisa diintip lewat URL langsung. |
| 2 | (int)$_SESSION['user_id'] | paksa jadi angka. session mestinya angka, tapi dipaksa lagi biar kalau session-nya dirusak jadi string aneh, query tidak ikut rusak. |
| 3 | $isAdmin = ($roleUser === 'admin') | bikin variabel saklar sekali, dipakai berkali-kali di bawah (query, tombol tambah, kolom hapus, catatan). biar tidak nulis $_SESSION['role'] === 'admin' lima kali. pakai === strict biar "admin " pakai spasi tidak lolos. |
| 4 | admin: mysqli_query('SELECT ... ORDER BY id ASC') | admin boleh lihat semua, jadi query tanpa WHERE itu wajar di sini. kolom password sengaja tidak di-select: tidak ada gunanya ditampilin, malah bahaya kalau ke-render. |
| 5 | user: SELECT ... WHERE id = ? + bind_param('i', ...) | user biasa cuma boleh ambil barisnya sendiri. ? + i (integer) biar id-nya aman. hasilnya dibungkus jadi array satu elemen [$row] biar loop foreach di bawah tidak perlu dua versi. |
| 6 | $_GET['pesan'] ?? '' + kotak hijau | pesan sukses dari tambah/edit/hapus (dashboard.php?pesan=User+berhasil...) ditampilin sekali. htmlspecialchars lagi biar pesan di URL tidak bisa disusupi script. |
| 7 | tombol [ + Tambah User Baru ] cuma kalau $isAdmin | user biasa tidak dikasih jalan pintas ke tambah.php. pertahanan lapis pertama di UI, lapis keduanya ada di tambah.php itu sendiri pakai wajibAdmin(). |
| 8 | link hapus.php disembunyiin kalau itu akun sendiri | admin tidak boleh hapus dirinya yang lagi dipakai. di UI diganti tulisan miring (Akun Anda). pertahanan lapis kedua ada di hapus.php yang nolak beneran. |
| 9 | onclick="return confirm(...)" | rem tangan browser: “yakin mau hapus?”. kalau klik Cancel, link tidak jalan. biar tidak kepencet, apalagi hapus itu permanen. |
9. tambah.php: Create Khusus Admin
huruf C di CRUD: Create. bedanya sama register: yang ini cuma admin yang boleh buka, dan bisa nentuin role.
<?php
require_once 'config.php';
wajibAdmin();
$error = '';
// 1. INPUT
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$nama = trim($_POST['nama'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$role = $_POST['role'] ?? 'user';
if (!in_array($role, ['admin', 'user'], true)) {
$role = 'user';
}
// 2. PROSES
if ($nama === '' || $email === '' || $password === '') {
$error = 'Semua field wajib diisi!';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$error = 'Format penulisan email tidak valid!';
} elseif (strlen($password) < 6) {
$error = 'Password minimal harus 6 karakter!';
} else {
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = mysqli_prepare($conn, 'INSERT INTO users (nama, email, password, role) VALUES (?, ?, ?, ?)');
mysqli_stmt_bind_param($stmt, 'ssss', $nama, $email, $hash, $role);
if (mysqli_stmt_execute($stmt)) {
mysqli_stmt_close($stmt);
// 3. OUTPUT
header('Location: dashboard.php?pesan=User+berhasil+ditambahkan');
exit;
} else {
$error = 'Gagal menambahkan user. Kemungkinan email sudah digunakan.';
mysqli_stmt_close($stmt);
}
}
}
?>
Penjelasan Detail Per Baris tambah.php
| Baris | Kode | Artinya + alasannya |
|---|---|---|
| 1 | wajibAdmin(); | beda sama file lain yang cuma wajibLogin(). yang ini khusus admin. user biasa yang nekat buka URL-nya langsung kena die('Akses ditolak'). |
| 2 | in_array($role, ['admin','user'], true) | whitelist role. user bisa edit HTML form-nya dan kirim role=superadmin, tapi di sini ditolak balik jadi user. true artinya strict: tipe datanya juga dicek. jangan pernah percaya input dropdown, karena dropdown bisa diubah dari DevTools. |
| 3 | tidak ada cek duplikat manual kayak register | di sini duplikatnya ketahuan dari gagalnya INSERT (karena kolom email UNIQUE). error-nya langsung dibaca sebagai “kemungkinan email sudah digunakan”. lebih pendek, dan tetap bener karena database yang jadi wasit terakhir. |
| 4 | redirect bawa ?pesan=User+berhasil+ditambahkan | biar dashboard bisa nampilin kotak hijau. + di URL otomatis jadi spasi. pola yang sama dipakai habis edit dan hapus, jadi user selalu dapat kepastian “tadi berhasil”. |
10. edit.php: Update + Siapa Boleh Ubah Siapa
ini file paling panjang dan paling gampang bikin pusing, karena ngerjain dua arah: GET buat nampilin data lama, POST buat nyimpan yang baru. plus ada aturan “siapa boleh ubah siapa”.
<?php
require_once 'config.php';
wajibLogin();
// 1. INPUT TAHAP 1 (ID dari URL)
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) {
die('ID pengguna tidak valid.');
}
// User biasa DILARANG mengedit user lain
if ($_SESSION['role'] !== 'admin' && $id !== (int)$_SESSION['user_id']) {
die('Akses ditolak: Anda hanya berhak mengubah data Anda sendiri.');
}
// Ambil data saat ini untuk ditampilkan di form
$stmt = mysqli_prepare($conn, 'SELECT id, nama, email, role FROM users WHERE id = ? LIMIT 1');
mysqli_stmt_bind_param($stmt, 'i', $id);
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
$user = mysqli_fetch_assoc($res);
mysqli_stmt_close($stmt);
if (!$user) {
die('Data user tidak ditemukan di database.');
}
?>
lanjutannya saat form di-submit:
<?php
// 1. INPUT TAHAP 2 (data baru dari POST)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$nama = trim($_POST['nama'] ?? '');
$email = trim($_POST['email'] ?? '');
$passwordBaru = $_POST['password'] ?? '';
if ($_SESSION['role'] === 'admin') {
$role = $_POST['role'] ?? $user['role'];
if (!in_array($role, ['admin', 'user'], true)) {
$role = 'user';
}
} else {
$role = $user['role'];
}
// 2. PROSES
if ($nama === '' || $email === '') {
$error = 'Nama dan email tidak boleh kosong!';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$error = 'Format penulisan email tidak valid!';
} else {
// Cek email dipakai user LAIN atau tidak
$stmt = mysqli_prepare($conn, 'SELECT id FROM users WHERE email = ? AND id <> ? LIMIT 1');
mysqli_stmt_bind_param($stmt, 'si', $email, $id);
mysqli_stmt_execute($stmt);
mysqli_stmt_store_result($stmt);
$emailSudahDipakai = mysqli_stmt_num_rows($stmt) > 0;
mysqli_stmt_close($stmt);
if ($emailSudahDipakai) {
$error = 'Email tersebut sudah digunakan oleh akun lain!';
} else {
if ($passwordBaru !== '') {
if (strlen($passwordBaru) < 6) {
$error = 'Password baru minimal harus 6 karakter!';
} else {
$hash = password_hash($passwordBaru, PASSWORD_DEFAULT);
$stmt = mysqli_prepare($conn, 'UPDATE users SET nama=?, email=?, password=?, role=? WHERE id=?');
mysqli_stmt_bind_param($stmt, 'ssssi', $nama, $email, $hash, $role, $id);
}
} else {
$stmt = mysqli_prepare($conn, 'UPDATE users SET nama=?, email=?, role=? WHERE id=?');
mysqli_stmt_bind_param($stmt, 'sssi', $nama, $email, $role, $id);
}
if ($error === '') {
if (mysqli_stmt_execute($stmt)) {
mysqli_stmt_close($stmt);
if ($id === (int)$_SESSION['user_id']) {
$_SESSION['nama'] = $nama;
$_SESSION['email'] = $email;
$_SESSION['role'] = $role;
}
// 3. OUTPUT
header('Location: dashboard.php?pesan=Data+berhasil+diperbarui');
exit;
}
}
}
}
}
?>
Penjelasan Detail Per Baris edit.php
| Baris | Kode | Artinya + alasannya |
|---|---|---|
| 1 | $id = (int)($_GET['id'] ?? 0) | ambil id dari URL edit.php?id=5 dan paksa jadi angka. alasannya hacker bisa kirim id=abc' OR 1=1, tapi habis di-(int) jadi 0 dan langsung ditolak. murah tapi ampuh. |
| 2 | if ($id <= 0) die(...) | tolak id nol, minus, atau kosong. jangan lanjut query dengan id sampah, nanti hasilnya ngaco. |
| 3 | if ($_SESSION['role'] !== 'admin' && $id !== ...) | aturan emasnya: user biasa cuma boleh buka id miliknya sendiri. admin bebas buka semua. dicek di awal, sebelum query, biar data orang lain tidak sempat kebaca sama sekali. |
| 4 | SELECT id, nama, email, role ... LIMIT 1 | ambil data lama buat ditampilin di form. password sengaja tidak diambil: tidak ada gunanya, hash tidak bisa ditampilin. makanya di form password dikosongin dengan tulisan “kosongkan jika tidak ingin diganti”. |
| 5 | role: admin boleh ganti, user dikunci | kalau bukan admin, $role dipaksa balik ke role lama. alasannya biar user tidak bisa naik pangkat sendiri jadi admin lewat DevTools. |
| 6 | SELECT id FROM users WHERE email = ? AND id <> ? | cek duplikat yang adil: email boleh sama kalau itu email dirinya sendiri, tapi tidak boleh sama dengan milik orang lain. <> artinya “tidak sama dengan”. tanpa ini, user yang cuma ganti nama doang bakal ditolak gara-gara emailnya “dianggap duplikat” oleh dirinya sendiri. |
| 7 | dua jalur UPDATE (dengan dan tanpa password) | kalau kolom password diisi, update 4 kolom termasuk password yang sudah di-hash. kalau dikosongin, update 3 kolom aja, password lama tetap. alasannya biar password tidak ke-reset jadi kosong gara-gara user tidak niat ganti. |
| 8 | bind_param('ssssi', ...) vs ('sssi', ...) | hurufnya harus pas sama jumlah ?: s = string, i = integer (buat id). kalau salah urut atau kurang satu, datanya nyasar ke kolom lain. ini sumber bug paling sering di file ini. |
| 9 | update $_SESSION kalau yang diubah akun sendiri | bayangin Budi ganti namanya jadi “Budi Ganteng”, tapi session masih nyimpan “Budi Santoso” — dashboard bakal nampilin nama lama sampai logout. makanya session di-sync ulang. |
| 10 | form action edit.php?id=... | id dibawa lewat URL, datanya lewat POST. alasannya: id itu penanda “mau ubah yang mana”, datanya itu “jadi apa”. dua-duanya dibaca ulang di server, tidak ada yang dipercaya mentah-mentah. |
form-nya juga ada trik kecil: dropdown role buat admin pakai selected, buat user biasa cuma teks + tulisan “(Hanya admin yang bisa mengganti hak akses)”. UX-nya jujur: user tahu kenapa dia tidak bisa ganti.
11. hapus.php + logout.php: Delete + Keluar
dua file paling pendek, tapi salah satunya paling bahaya kalau salah.
<?php
// hapus.php
require_once 'config.php';
wajibAdmin();
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) {
die('ID pengguna tidak valid.');
}
// Admin tidak boleh menghapus akunnya sendiri
if ($id === (int)$_SESSION['user_id']) {
die('Akses ditolak: Anda tidak bisa menghapus akun yang sedang Anda gunakan.');
}
$stmt = mysqli_prepare($conn, 'DELETE FROM users WHERE id = ?');
mysqli_stmt_bind_param($stmt, 'i', $id);
mysqli_stmt_execute($stmt);
mysqli_stmt_close($stmt);
header('Location: dashboard.php?pesan=User+berhasil+dihapus');
exit;
<?php
// logout.php
require_once 'config.php';
session_unset();
session_destroy();
header('Location: login.php');
exit;
Penjelasan Detail Per Baris hapus.php + logout.php
| Baris | Kode | Artinya + alasannya |
|---|---|---|
| 1 | wajibAdmin() di hapus | tombol hapus cuma buat admin. user biasa yang nekat ketik URL-nya manual langsung ditolak. ingat: sembunyiin tombol di UI saja tidak cukup, server harus nolak juga. |
| 2 | tolak hapus diri sendiri | kalau admin hapus akunnya yang lagi dipakai, tidak ada admin tersisa + session yatim. makanya ditolak keras pakai die. ini bedanya sama artikel CRUD mahasiswa yang tombol hapusnya bebas. |
| 3 | DELETE FROM users WHERE id = ? | huruf D di CRUD. WHERE id = ? itu wajib. tanpa WHERE, semua baris kehapus. ini query paling serem, makanya id-nya di-(int) dulu + pakai prepared statement. |
| 4 | redirect ?pesan=User+berhasil+dihapus | kasih kepastian ke admin kalau hapusnya beneran jalan. tanpa ini admin bingung “tadi kehapus nggak sih?”. |
| 5 | session_unset() + session_destroy() di logout | unset = kosongin isi tas, destroy = bakar tasnya di server. dua-duanya dipakai biar benar-benar keluar. kalau cuma redirect tanpa ini, session masih nempel dan user masih dianggap login. |
| 6 | habis logout tendang ke login.php | jangan ke dashboard, karena dashboard bakal nendang balik juga (belum login). langsung ke login itu jalur paling pendek. |
12. Deploy dari Localhost Sampai Online
bagian ini ngikutin naskah video H4 bab 5-6. Vercel dan GitHub Pages tidak bisa buat PHP, jadi kita pakai hosting PHP + MySQL.
opsi gratis buat belajar: InfinityFree — sudah support PHP 8 + MySQL + SSL gratis. kalau mau yang berbayar murah: Hostinger, Niagahoster, RumahWeb. polanya sama semua.
langkahnya:
- Export database lokal: buka phpMyAdmin di
http://localhost/phpmyadmin→ pilihbelajar_crud→ tab Export → Go. dapat file.sql. - Siapin koper: blok semua file
.phpdi folderbelajar-web(bukan foldernya), klik kanan → Compress ke ZIP. ingat, yang diblok isinya, bukan foldernya — ini poin yang sering bikin extract-nya jadi folder ganda di hosting. - Upload: login hosting → File Manager → buka
htdocs(di cPanel namanyapublic_html) → upload ZIP → Extract di situ. - Bikin database hosting: buka menu MySQL Databases → bikin database + user baru → catat nama host, username, password, nama database. ini poin paling sering bikin nyerah, jangan sampai salah catat.
- Import: buka phpMyAdmin versi hosting → pilih database baru → Import → pilih file
.sqltadi. - Ubah
config.phpdi hosting:
$DB_HOST = "sqlxxx.infinityfree.com"; // lihat di dashboard hosting, bukan localhost
$DB_USER = "if0_12345678";
$DB_PASS = "password_hosting_kalian";
$DB_NAME = "if0_12345678_belajar_crud";
- Aktifin SSL + tes: minta Free SSL di panel hosting, aktifin Force HTTPS, terus buka URL-nya di HP pakai data seluler (bukan wifi yang sama). coba daftar akun baru, login sebagai admin, edit, hapus. kalau bisa, berarti sudah online beneran.
kalau mau custom domain kayak crud-sekolahku.my.id, tinggal arahin nameserver-nya ke hosting di tempat beli domain. file PHP-nya tidak perlu diubah.
13. Error Paling Sering + Best Practice
| Pesan / Gejala | Penyebab | Solusi 1 menit |
|---|---|---|
Koneksi ke database gagal | user/pass/nama DB di config.php salah. di lokal root tanpa password, di hosting beda. | samain config.php dengan data dari dashboard hosting. di lokal cek MySQL-nya nyala belum. |
Headers already sent | ada echo atau spasi sebelum header() / session_start(). ibarat surat sudah dikirim baru mau ganti alamat. | session_start() dan header() harus sebelum HTML apa pun. cek tidak ada spasi/BOM sebelum <?php. |
ID pengguna tidak valid terus | buka edit.php tanpa ?id= atau id-nya huruf. | buka lewat link dari dashboard, jangan ketik manual. (int) bikin huruf jadi 0 dan memang ditolak. |
Akses ditolak padahal merasa admin | login pakai akun user biasa, atau role di session masih lama. | logout lalu login lagi pakai [email protected]. cek di dashboard tertulis Peran: admin. |
Email sudah digunakan | email duplikat. kolom email itu UNIQUE, database yang nolak. | pakai email lain, atau edit akun yang sudah ada. di edit.php email sendiri boleh dipakai lagi, email orang lain tidak. |
| login bener tapi mental lagi ke login | session_start() lupa / cookie mati / redirect loop. | pastikan config.php di-require di semua file dan session_start() jalan. coba incognito. |
500 di hosting | config.php masih localhost / versi PHP hosting terlalu tua. | ganti host/user/pass ke data hosting, pilih PHP 8.x di panel. |
| foto tidak ada? tenang | project ini memang tidak ada upload file. kalau butuh upload, lanjut ke cara upload file dengan PHP dan membuat website dari nol sampai online yang bahas $_FILES. | jangan campur dulu, kuasai CRUD + login ini sampai lancar. |
best practice versi industri (mulai biasain dari sekarang):
- jangan percaya input user. HTML
requireditu hiasan, validasi benerannya di PHP (trim,filter_var,strlen) + database (UNIQUE,ENUM,NOT NULL). ibarat periksa KTP tiga kali: di gerbang, resepsionis, arsip. - password selalu hash. bikin akun manual di database? hash dulu pakai
password_hash, jangan tulis plain text. login selalupassword_verify, jangan==. - query selalu prepared statement. tiap ada
?+bind_param, kalian aman dari SQL injection. pengecualiannya cuma query tanpa input user sama sekali (kayakSELECT ... ORDER BY id ASCbuat admin di dashboard). - otorisasi di server, bukan di tombol. sembunyiin tombol hapus itu UX,
wajibAdmin()+ cek id itu keamanan. dua-duanya dipasang, kayak didashboard.php+hapus.php. - redirect habis POST sukses. biar refresh tidak ngirim dobel. gagal? tampilin lagi formnya +
$error-nya. - kredensial jangan ke-push GitHub.
config.phpisi password hosting? masukin ke.gitignore. kalau sudah naik level pakai.env.
kalau mau naik level habis ini: cara membuat login multi user php buat pendalaman role, tutorial mysql lengkap buat ngerti tipe data, dan membuat crud php mysqli buat versi studi kasus lain (mahasiswa).
14. Kesimpulan
kita baru aja bangun aplikasi web beneran dari nol sampai online: daftar akun yang otomatis jadi user, login yang ngasih gelang session, dashboard yang beda isi buat admin dan user, tambah khusus admin, edit yang tahu siapa boleh ubah siapa, hapus yang tidak boleh bunuh diri, sampai logout yang bersih. polanya selalu sama: input → proses → output.
kalau alur ini sudah nempel di kepala, ganti tabel users jadi siswa, barang, atau anggota OSIS tinggal ganti nama kolom dan label formnya aja. mesinnya sama. dan itu persis yang dipakai di dunia industri buat PPDB, absensi, kasir, sampai admin panel: form → database → tabel → hak akses → hosting.
coba sekarang: daftar satu akun baru, login sebagai admin, ubah role-nya, edit datanya, hapus, logout. kalau semua lancar tanpa lihat contekan, berarti kalian sudah lulus modul ini. lanjut ke upload file atau deploy custom domain, happy coding!
Pertanyaan yang Sering Diajukan
Apa itu konsep IPO di PHP?
IPO singkatan dari Input - Proses - Output. Input itu data yang masuk dari form atau URL, Proses itu validasi dan query ke MySQL, Output itu tampilannya: redirect kalau sukses atau form lagi kalau gagal. Semua file di tutorial ini saya kasih label IPO biar alurnya kebaca.
Kenapa harus pakai prepared statement, bukan query string biasa?
Karena query string biasa seperti "SELECT * FROM users WHERE email=$email" bisa dibobol pakai SQL injection. Prepared statement memisahkan perintah SQL dan datanya lewat tanda tanya (?), jadi input jahat dianggap teks biasa, bukan perintah.
Kenapa password wajib pakai password_hash dan password_verify?
Karena password tidak boleh disimpan apa adanya. password_hash mengacaknya satu arah sehingga database bocor pun password asli tetap aman. Saat login, password_verify mencocokkan ketikan user dengan hash tersebut tanpa perlu membaca balik hash-nya.
Apa beda hak akses admin dan user di project ini?
Admin bisa lihat semua data, tambah user, edit semua, dan hapus user lain. User biasa cuma bisa lihat dan edit datanya sendiri. Aturannya dijaga di dashboard.php, edit.php, tambah.php, dan hapus.php lewat session role.
Kenapa admin tidak boleh hapus akunnya sendiri?
Biar tidak kekunci di luar. Kalau akun yang sedang dipakai malah dihapus, session-nya jadi yatim dan tidak ada admin tersisa. Makanya hapus.php menolak kalau id yang dihapus sama dengan id di session.
Kenapa website PHP tidak bisa deploy ke Vercel atau GitHub Pages?
Vercel dan GitHub Pages cuma bisa jalanin file statis HTML CSS JS. PHP butuh interpreter di server plus MySQL, jadi butuh hosting seperti InfinityFree, Hostinger, Niagahoster, atau VPS dengan Apache atau Nginx.

