Bikin CRUD PHP Native yang Aman: Dari Koneksi sampai Validasi
CRUD — Create, Read, Update, Delete — adalah fondasi dari hampir semua aplikasi web. Kelihatannya sederhana, dan memang secara konsep tidak rumit. Tapi implementasinya sering jadi pintu masuk masalah keamanan kalau tidak dikerjakan dengan benar.
Saya pernah review kode milik teman yang baru belajar PHP. CRUD-nya jalan, tapi ada SQL Injection di mana-mana, password disimpan plaintext, dan tidak ada validasi input sama sekali. Secara fungsional bagus, secara keamanan berbahaya. Artikel ini adalah versi yang seharusnya — CRUD yang tidak hanya jalan, tapi juga aman.
Struktur Proyek
Saya suka mulai dari struktur folder yang rapi sebelum mulai coding. Ini yang biasanya saya pakai untuk proyek PHP sederhana:
/proyek
/config
database.php ← konfigurasi dan koneksi DB
/src
functions.php ← helper functions
/public
index.php ← halaman utama (Read)
tambah.php ← form + proses Create
edit.php ← form + proses Update
hapus.php ← proses Delete
.htaccess ← proteksi direktori
File konfigurasi dipisah dari public folder — ini penting agar file sensitif tidak bisa diakses langsung lewat browser.
Koneksi Database yang Benar
Gunakan PDO, bukan mysqli. PDO lebih fleksibel, mendukung berbagai database, dan prepared statement-nya lebih rapi:
<?php
// config/database.php
function getDB(): PDO {
static $pdo = null;
if ($pdo === null) {
$dsn = "mysql:host=localhost;dbname=nama_db;charset=utf8mb4";
try {
$pdo = new PDO($dsn, "userapp", "password_kuat", [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
} catch (PDOException $e) {
error_log($e->getMessage());
die("Koneksi database gagal.");
}
}
return $pdo;
}
Perhatikan PDO::ATTR_EMULATE_PREPARES => false — ini memastikan prepared statement benar-benar diproses di sisi database, bukan di-emulasi oleh PDO. Perbedaannya penting untuk keamanan.
Create: Tambah Data dengan Aman
<?php
// public/tambah.php
require_once "../config/database.php";
require_once "../src/functions.php";
$errors = [];
$data = ["nama" => "", "email" => "", "pesan" => ""];
if ($_SERVER["REQUEST_METHOD"] === "POST") {
// Sanitasi input
$data["nama"] = trim(htmlspecialchars($_POST["nama"] ?? "", ENT_QUOTES, "UTF-8"));
$data["email"] = trim($_POST["email"] ?? "");
$data["pesan"] = trim(htmlspecialchars($_POST["pesan"] ?? "", ENT_QUOTES, "UTF-8"));
// Validasi
if (empty($data["nama"])) {
$errors["nama"] = "Nama tidak boleh kosong";
} elseif (mb_strlen($data["nama"]) > 100) {
$errors["nama"] = "Nama terlalu panjang (maks 100 karakter)";
}
if (!filter_var($data["email"], FILTER_VALIDATE_EMAIL)) {
$errors["email"] = "Format email tidak valid";
}
if (empty($errors)) {
$db = getDB();
$stmt = $db->prepare("INSERT INTO kontak (nama, email, pesan, created_at) VALUES (?, ?, ?, NOW())");
$stmt->execute([$data["nama"], $data["email"], $data["pesan"]]);
header("Location: index.php?pesan=berhasil");
exit;
}
}
Beberapa hal yang saya lakukan di sini: sanitasi dengan htmlspecialchars untuk mencegah XSS, validasi format email dengan filter_var, dan menggunakan prepared statement untuk mencegah SQL Injection. Tidak ada input user yang langsung masuk ke query.
Read: Tampilkan Data dengan Pagination
<?php
// public/index.php
require_once "../config/database.php";
$halaman = max(1, (int)($_GET["hal"] ?? 1));
$perHalaman = 15;
$offset = ($halaman - 1) * $perHalaman;
$db = getDB();
// Hitung total untuk pagination
$total = $db->query("SELECT COUNT(*) FROM kontak")->fetchColumn();
$totalHalaman = (int)ceil($total / $perHalaman);
// Ambil data halaman ini saja
$stmt = $db->prepare("SELECT id, nama, email, pesan, created_at FROM kontak ORDER BY created_at DESC LIMIT ? OFFSET ?");
$stmt->execute([$perHalaman, $offset]);
$data = $stmt->fetchAll();
Update: Edit Data dengan Validasi ID
<?php
// public/edit.php
$id = filter_input(INPUT_GET, "id", FILTER_VALIDATE_INT);
if (!$id || $id <= 0) {
header("Location: index.php");
exit;
}
$db = getDB();
$stmt = $db->prepare("SELECT * FROM kontak WHERE id = ?");
$stmt->execute([$id]);
$item = $stmt->fetch();
if (!$item) {
header("Location: index.php?pesan=tidak_ditemukan");
exit;
}
Validasi ID itu krusial. Kalau tidak divalidasi, orang bisa mengirim ID berapa saja lewat URL dan mencoba mengakses atau memodifikasi data yang bukan haknya.
Delete: Tambahkan Konfirmasi dan CSRF Token
Hapus data tidak boleh bisa dilakukan hanya dengan mengakses URL. Minimal harus ada konfirmasi, dan idealnya ada CSRF token untuk mencegah serangan Cross-Site Request Forgery:
<?php
session_start();
// Generate CSRF token kalau belum ada
if (!isset($_SESSION["csrf_token"])) {
$_SESSION["csrf_token"] = bin2hex(random_bytes(32));
}
if ($_SERVER["REQUEST_METHOD"] === "POST") {
// Validasi CSRF token
if (!hash_equals($_SESSION["csrf_token"], $_POST["csrf_token"] ?? "")) {
die("Request tidak valid.");
}
$id = filter_input(INPUT_POST, "id", FILTER_VALIDATE_INT);
if ($id && $id > 0) {
$db = getDB();
$stmt = $db->prepare("DELETE FROM kontak WHERE id = ?");
$stmt->execute([$id]);
}
header("Location: index.php?pesan=terhapus");
exit;
}
Satu Hal yang Sering Dilupakan: Error Handling
Jangan pernah tampilkan error PHP mentah ke pengguna di production. Konfigurasi ini di php.ini atau di awal script:
<?php
ini_set("display_errors", 0);
ini_set("log_errors", 1);
ini_set("error_log", "/var/log/php_errors.log");
Error tetap dilog untuk debugging, tapi tidak ditampilkan ke pengguna. Pesan error yang detail bisa memberikan informasi berharga bagi penyerang — struktur database, path file, versi PHP, dan lain-lain.
Kesimpulan
CRUD yang aman bukan berarti CRUD yang kompleks. Intinya ada di beberapa kebiasaan yang perlu ditanamkan dari awal: selalu pakai prepared statement, selalu validasi dan sanitasi input, selalu validasi ID sebelum query, dan jangan pernah tampilkan error mentah ke pengguna.
Kebiasaan yang baik itu jauh lebih mudah ditanamkan di awal daripada diperbaiki setelah aplikasi sudah besar dan sudah ada pengguna. 🙂
Belum ada komentar. Jadilah yang pertama menulis.
Tulis Komentar