Membangun CMS Sederhana dengan PHP & MySQL - Part 3: Manajemen Artikel (CRUD) di Area Admin
Selamat datang di Bagian 3 seri tutorial CMS PHP kita! Di Part 1, kita telah membangun frontend untuk menampilkan artikel, dan di Part 2, kita berhasil membuat sistem login yang aman serta dashboard admin dasar. Sekarang, saatnya memberikan “nyawa” pada area admin dengan mengimplementasikan fungsionalitas inti: Manajemen Artikel.
Dalam bagian ini, kita akan fokus pada operasi CRUD (Create, Read, Update, Delete) untuk artikel. Kita akan membuat halaman-halaman berikut di dalam area admin:
- Halaman Daftar Artikel (
manage_posts.php
): Menampilkan semua artikel (termasuk draft) dengan opsi untuk menambah, mengedit, atau menghapus. - Halaman Tambah Artikel (
add_post.php
): Formulir untuk membuat artikel baru. - Halaman Edit Artikel (
edit_post.php
): Formulir untuk memodifikasi artikel yang sudah ada. - Skrip Hapus Artikel (
delete_post.php
): Logika untuk menghapus artikel.
Kita juga akan membahas validasi input dasar dan penggunaan pesan notifikasi (flash messages) untuk umpan balik kepada pengguna.
Prasyarat
Pastikan Anda telah mengikuti dan memahami Part 1 dan Part 2 dari seri ini. Kita akan melanjutkan dari struktur dan fungsionalitas yang sudah ada.
Persiapan: Fungsi Helper dan Pesan Notifikasi
Sebelum membuat halaman CRUD, mari tambahkan beberapa hal yang akan berguna.
1. Perbarui includes/functions.php
(di root proyek):
Kita akan menambahkan fungsi untuk membuat slug otomatis dari judul.
<?php
// includes/functions.php (Tambahkan fungsi baru ini)
// ... (fungsi generate_excerpt yang sudah ada) ...
/**
* Membuat slug yang ramah URL dari sebuah string.
*
* @param string $text Teks sumber (biasanya judul).
* @return string Slug yang dihasilkan.
*/
function generate_slug($text) {
// 1. Ubah ke huruf kecil
$text = strtolower($text);
// 2. Ganti spasi dan karakter non-alfanumerik dengan strip (-)
$text = preg_replace('/[^a-z0-9]+/', '-', $text);
// 3. Hapus strip berlebih di awal atau akhir
$text = trim($text, '-');
// 4. (Opsional) Batasi panjang slug jika perlu
// $text = substr($text, 0, 70);
// $text = trim($text, '-'); // Hapus lagi jika pemotongan menghasilkan strip di akhir
if (empty($text)) {
return 'n-a-' . time(); // Slug default jika judul sangat aneh
}
return $text;
}
/**
* Menampilkan pesan notifikasi (flash message).
* Pesan disimpan di sesi dan dihapus setelah ditampilkan.
*
* @param string $name Nama unik untuk pesan (misal, 'success', 'error').
* @param string $message Pesan yang akan ditampilkan.
* @param string $type Kelas CSS untuk styling pesan (misal, 'success', 'error', 'info').
*/
function set_flash_message($name, $message, $type = 'success') {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
$_SESSION['flash_messages'][$name] = ['message' => $message, 'type' => $type];
}
function display_flash_message($name) {
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (isset($_SESSION['flash_messages'][$name])) {
$flash_message = $_SESSION['flash_messages'][$name];
echo '<div class="flash-message ' . htmlspecialchars($flash_message['type']) . '">' . htmlspecialchars($flash_message['message']) . '</div>';
unset($_SESSION['flash_messages'][$name]); // Hapus pesan setelah ditampilkan
}
}
?>
2. Tambahkan CSS untuk Flash Messages di css/admin_style.css
:
/* css/admin_style.css (Tambahkan di akhir file) */
.flash-message {
padding: 15px;
margin-bottom: 20px;
border-radius: 5px;
font-weight: 500;
text-align: center;
}
.flash-message.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.flash-message.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.flash-message.info {
background-color: #cce5ff;
color: #004085;
border: 1px solid #b8daff;
}
Langkah 1: Halaman Daftar Artikel Admin (admin/manage_posts.php
)
Halaman ini akan menampilkan tabel semua artikel (baik yang sudah publish maupun draft) dan menyediakan link untuk menambah, mengedit, serta menghapus.
Buat file admin/manage_posts.php
:
<?php
// admin/manage_posts.php
require_once 'auth_check.php'; // Otentikasi
require_once '../includes/db_connect.php'; // Koneksi DB
require_once '../includes/functions.php'; // Untuk flash messages
$page_title_admin = "Kelola Artikel";
// (Opsional) Sertakan header admin jika ada
// require_once 'includes/header_admin.php';
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($page_title_admin); ?> - CMS Admin</title>
<link rel="stylesheet" href="../css/admin_style.css">
</head>
<body>
<header class="admin-header">
<h1>Admin Panel</h1>
<div>
Selamat datang, <?php echo htmlspecialchars($current_username); ?> |
<a href="logout.php">Logout</a>
</div>
</header>
<div class="admin-container">
<nav class="admin-nav">
<ul>
<li><a href="index.php">Dashboard</a></li>
<li><a href="manage_posts.php" class="active">Kelola Artikel</a></li>
<li><a href="manage_categories.php">Kelola Kategori</a></li>
<?php if ($current_user_role === 'admin'): ?>
<li><a href="manage_users.php">Kelola Pengguna</a></li>
<?php endif; ?>
<li><a href="../index.php" target="_blank">Lihat Situs</a></li>
</ul>
</nav>
<h2><?php echo htmlspecialchars($page_title_admin); ?></h2>
<?php display_flash_message('post_action'); // Tampilkan pesan notifikasi ?>
<a href="add_post.php" class="btn btn-add">Tambah Artikel Baru</a>
<table class="admin-table">
<thead>
<tr>
<th>ID</th>
<th>Judul</th>
<th>Slug</th>
<th>Status</th>
<th>Tanggal Dibuat</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
<?php
$sql = "SELECT id, title, slug, status, created_at FROM posts ORDER BY created_at DESC";
$result = mysqli_query($conn, $sql);
if (mysqli_num_rows($result) > 0) {
while ($post = mysqli_fetch_assoc($result)) {
echo "<tr>";
echo "<td>" . $post['id'] . "</td>";
echo "<td>" . htmlspecialchars($post['title']) . "</td>";
echo "<td>" . htmlspecialchars($post['slug']) . "</td>";
echo "<td>" . ucfirst(htmlspecialchars($post['status'])) . "</td>"; // ucfirst untuk kapitalisasi awal status
echo "<td>" . date('d M Y, H:i', strtotime($post['created_at'])) . "</td>";
echo "<td>
<a href='edit_post.php?id=" . $post['id'] . "' class='btn btn-edit'>Edit</a>
<a href='delete_post.php?id=" . $post['id'] . "' class='btn btn-delete' onclick='return confirm(\"Apakah Anda yakin ingin menghapus artikel ini?\");'>Hapus</a>
</td>";
echo "</tr>";
}
} else {
echo "<tr><td colspan='6' style='text-align:center;'>Belum ada artikel.</td></tr>";
}
?>
</tbody>
</table>
</div>
<?php
// (Opsional) Sertakan footer admin
// require_once 'includes/footer_admin.php';
if(isset($conn)) { mysqli_close($conn); }
?>
</body>
</html>
Penjelasan admin/manage_posts.php
:
- Otentikasi dan Koneksi: Seperti biasa.
- Navigasi Aktif: Menambahkan kelas
active
ke link “Kelola Artikel”. - Flash Message:
display_flash_message('post_action');
akan menampilkan notifikasi sukses/error dari operasi tambah/edit/hapus artikel. - Tombol Tambah Artikel: Link ke
add_post.php
. - Tabel Artikel: Menampilkan data artikel.
ucfirst()
digunakan untuk membuat huruf pertama status menjadi kapital. - Tombol Aksi:
- Link Edit mengarah ke
edit_post.php
denganid
artikel. - Link Hapus mengarah ke
delete_post.php
denganid
artikel.onclick='return confirm(...)'
adalah konfirmasi JavaScript sederhana sebelum menghapus.
- Link Edit mengarah ke
Tambahkan CSS untuk Tombol dan Tabel di css/admin_style.css
:
/* css/admin_style.css (Tambahkan atau modifikasi) */
/* ... (CSS sebelumnya) ... */
.btn { /* Gaya dasar untuk tombol jika belum ada */
padding: 8px 15px;
text-decoration: none;
border-radius: 4px;
color: white;
font-weight: 500;
display: inline-block;
margin-right: 5px;
border: none;
cursor: pointer;
}
.btn-add { background-color: #28a745; margin-bottom: 20px;} /* Hijau untuk tambah */
.btn-add:hover { background-color: #218838; }
.btn-edit { background-color: #ffc107; color: #212529;} /* Kuning untuk edit */
.btn-edit:hover { background-color: #e0a800; }
.btn-delete { background-color: #dc3545; } /* Merah untuk hapus */
.btn-delete:hover { background-color: #c82333; }
.admin-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.admin-table th, .admin-table td {
border: 1px solid #ddd;
padding: 10px 12px;
text-align: left;
vertical-align: middle;
}
.admin-table thead th {
background-color: #f8f9fa;
font-weight: 600;
color: #495057;
}
.admin-table tbody tr:nth-child(even) {
background-color: #fdfdfd;
}
.admin-table tbody tr:hover {
background-color: #f1f1f1;
}
.admin-table td:last-child { /* Kolom aksi */
text-align: center;
white-space: nowrap; /* Agar tombol tidak wrap */
}
Langkah 2: Halaman Tambah Artikel (admin/add_post.php
)
Formulir untuk membuat artikel baru.
Buat file admin/add_post.php
:
<?php
// admin/add_post.php
require_once 'auth_check.php';
require_once '../includes/db_connect.php';
require_once '../includes/functions.php';
$page_title_admin = "Tambah Artikel Baru";
$title = '';
$content = '';
$status = 'published'; // Default status
$slug = '';
$errors = []; // Array untuk menyimpan pesan error validasi
if ($_SERVER["REQUEST_METHOD"] == "POST") {
// Ambil dan sanitasi data dari form
$title = trim($_POST['title']);
$content = trim($_POST['content']); // Untuk editor WYSIWYG, sanitasi mungkin berbeda
$status = isset($_POST['status']) ? trim($_POST['status']) : 'draft';
$slug_custom = trim($_POST['slug']); // Slug kustom opsional
// Validasi dasar
if (empty($title)) {
$errors[] = "Judul tidak boleh kosong.";
}
if (empty($content)) {
$errors[] = "Konten tidak boleh kosong.";
}
if (!in_array($status, ['published', 'draft'])) {
$errors[] = "Status tidak valid.";
}
// Buat slug: gunakan slug kustom jika ada, jika tidak, generate dari judul
if (!empty($slug_custom)) {
$slug = generate_slug($slug_custom);
} else {
$slug = generate_slug($title);
}
// Cek keunikan slug (penting!)
if (!empty($slug)) {
$sql_check_slug = "SELECT id FROM posts WHERE slug = ?";
if($stmt_check = mysqli_prepare($conn, $sql_check_slug)) {
mysqli_stmt_bind_param($stmt_check, "s", $slug);
mysqli_stmt_execute($stmt_check);
mysqli_stmt_store_result($stmt_check);
if(mysqli_stmt_num_rows($stmt_check) > 0) {
$errors[] = "Slug '" . htmlspecialchars($slug) . "' sudah digunakan. Coba slug kustom atau ubah judul.";
}
mysqli_stmt_close($stmt_check);
}
} else if(empty($errors) && empty($title)) {
// Jika judul kosong dan slug juga jadi kosong, error.
// Tapi ini sudah ditangani oleh validasi judul kosong.
}
if (empty($errors)) {
// Jika tidak ada error, masukkan data ke database
$sql_insert = "INSERT INTO posts (title, content, slug, status, author_id) VALUES (?, ?, ?, ?, ?)";
if ($stmt = mysqli_prepare($conn, $sql_insert)) {
// Asumsikan author_id adalah user yang sedang login
$author_id = $_SESSION['user_id'];
mysqli_stmt_bind_param($stmt, "ssssi", $title, $content, $slug, $status, $author_id);
if (mysqli_stmt_execute($stmt)) {
set_flash_message('post_action', "Artikel \"" . htmlspecialchars($title) . "\" berhasil ditambahkan!", "success");
header("Location: manage_posts.php");
exit;
} else {
$errors[] = "Gagal menyimpan artikel: " . mysqli_stmt_error($stmt);
}
mysqli_stmt_close($stmt);
} else {
$errors[] = "Gagal mempersiapkan statement: " . mysqli_error($conn);
}
}
}
// mysqli_close($conn); // Tutup koneksi di akhir
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($page_title_admin); ?> - CMS Admin</title>
<link rel="stylesheet" href="../css/admin_style.css">
</head>
<body>
<header class="admin-header">
<h1>Admin Panel</h1>
<div>Selamat datang, <?php echo htmlspecialchars($current_username); ?> | <a href="logout.php">Logout</a></div>
</header>
<div class="admin-container">
<nav class="admin-nav">
<ul>
<li><a href="index.php">Dashboard</a></li>
<li><a href="manage_posts.php">Kelola Artikel</a></li>
<li><a href="manage_categories.php">Kelola Kategori</a></li>
<?php if ($current_user_role === 'admin'): ?>
<li><a href="manage_users.php">Kelola Pengguna</a></li>
<?php endif; ?>
</ul>
</nav>
<h2><?php echo htmlspecialchars($page_title_admin); ?></h2>
<?php if (!empty($errors)): ?>
<div class="flash-message error">
<strong>Error!</strong>
<ul>
<?php foreach ($errors as $error): ?>
<li><?php echo htmlspecialchars($error); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post" class="admin-form">
<div class="form-group">
<label for="title">Judul Artikel:</label>
<input type="text" name="title" id="title" value="<?php echo htmlspecialchars($title); ?>" required>
</div>
<div class="form-group">
<label for="slug">Slug (Opsional, akan dibuat otomatis jika kosong):</label>
<input type="text" name="slug" id="slug" value="<?php echo htmlspecialchars($slug); ?>" placeholder="contoh-slug-artikel-ini">
<small>Hanya huruf kecil, angka, dan strip (-).</small>
</div>
<div class="form-group">
<label for="content">Konten:</label>
<textarea name="content" id="content" rows="15" required><?php echo htmlspecialchars($content); ?></textarea>
<!-- Nanti bisa diganti dengan editor WYSIWYG seperti TinyMCE -->
</div>
<div class="form-group">
<label for="status">Status:</label>
<select name="status" id="status">
<option value="published" <?php echo ($status === 'published') ? 'selected' : ''; ?>>Published</option>
<option value="draft" <?php echo ($status === 'draft') ? 'selected' : ''; ?>>Draft</option>
</select>
</div>
<!-- Nanti bisa ditambahkan field untuk gambar unggulan dan kategori -->
<div class="form-actions">
<button type="submit" class="btn btn-submit">Simpan Artikel</button>
<a href="manage_posts.php" class="btn btn-cancel">Batal</a>
</div>
</form>
</div>
<?php if(isset($conn)) { mysqli_close($conn); } ?>
</body>
</html>
Penjelasan admin/add_post.php
:
- Variabel Form: Menyimpan nilai input form untuk ditampilkan kembali jika ada error (sticky form).
- Validasi: Pemeriksaan dasar untuk field yang wajib diisi.
- Slug Generation & Uniqueness: Menggunakan fungsi
generate_slug()
dan mengecek ke database apakah slug sudah ada. Ini penting agar URL artikel unik. - Prepared Statement untuk INSERT: Aman dan efisien.
author_id
: Diambil dari$_SESSION['user_id']
pengguna yang sedang login.- Flash Message dan Redirect: Setelah berhasil, pesan sukses ditampilkan di halaman
manage_posts.php
. - Menampilkan Error Validasi: Jika ada error, ditampilkan di atas formulir.
- Sticky Form: Nilai
value="<?php echo htmlspecialchars($title); ?>"
pada input memastikan data yang sudah diisi pengguna tidak hilang jika ada error validasi.
CSS untuk Form di css/admin_style.css
:
/* css/admin_style.css (Tambahkan) */
/* ... (CSS sebelumnya) ... */
.admin-form .form-group {
margin-bottom: 20px;
}
.admin-form label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #495057;
}
.admin-form input[type="text"],
.admin-form input[type="password"], /* Untuk form login atau ganti password */
.admin-form select,
.admin-form textarea {
width: 100%;
padding: 12px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 1rem;
color: #495057;
background-color: #fff;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.admin-form input[type="text"]:focus,
.admin-form input[type="password"]:focus,
.admin-form select:focus,
.admin-form textarea:focus {
border-color: #80bdff;
outline: 0;
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
}
.admin-form textarea {
min-height: 150px;
resize: vertical;
}
.admin-form small {
display: block;
margin-top: 5px;
font-size: 0.85em;
color: #6c757d;
}
.form-actions {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e9ecef;
text-align: right;
}
.btn-submit { background-color: #007bff; }
.btn-submit:hover { background-color: #0069d9; }
.btn-cancel { background-color: #6c757d; }
.btn-cancel:hover { background-color: #5a6268; }
Langkah 3: Halaman Edit Artikel (admin/edit_post.php
)
Mirip dengan tambah artikel, tapi formulir diisi dengan data artikel yang ada.
Buat file admin/edit_post.php
:
<?php
// admin/edit_post.php
require_once 'auth_check.php';
require_once '../includes/db_connect.php';
require_once '../includes/functions.php';
$page_title_admin = "Edit Artikel";
$post_id = isset($_GET['id']) ? (int)$_GET['id'] : 0; // Ambil ID artikel dari URL
// Variabel untuk menyimpan data post dan error
$post = null;
$title = '';
$content = '';
$status = '';
$slug = '';
$errors = [];
// 1. Ambil data artikel yang akan diedit dari database
if ($post_id > 0) {
$sql_select = "SELECT title, content, slug, status FROM posts WHERE id = ?";
if ($stmt_select = mysqli_prepare($conn, $sql_select)) {
mysqli_stmt_bind_param($stmt_select, "i", $post_id);
mysqli_stmt_execute($stmt_select);
$result_select = mysqli_stmt_get_result($stmt_select);
if ($post_data = mysqli_fetch_assoc($result_select)) {
$post = $post_data; // Simpan data asli untuk referensi
$title = $post_data['title'];
$content = $post_data['content'];
$status = $post_data['status'];
$slug = $post_data['slug'];
} else {
set_flash_message('post_action', "Artikel dengan ID $post_id tidak ditemukan.", "error");
header("Location: manage_posts.php");
exit;
}
mysqli_stmt_close($stmt_select);
} else {
die("Error mempersiapkan statement SELECT: " . mysqli_error($conn));
}
} else {
set_flash_message('post_action', "ID Artikel tidak valid.", "error");
header("Location: manage_posts.php");
exit;
}
// 2. Proses form jika disubmit
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$title_new = trim($_POST['title']);
$content_new = trim($_POST['content']);
$status_new = isset($_POST['status']) ? trim($_POST['status']) : $status; // Gunakan status lama jika tidak diubah
$slug_new_custom = trim($_POST['slug']);
// Validasi
if (empty($title_new)) $errors[] = "Judul tidak boleh kosong.";
if (empty($content_new)) $errors[] = "Konten tidak boleh kosong.";
if (!in_array($status_new, ['published', 'draft'])) $errors[] = "Status tidak valid.";
// Update slug jika judul berubah atau slug kustom diisi
// Jika slug kustom diisi, gunakan itu. Jika tidak, generate dari judul baru.
// Hanya generate slug baru jika judul berubah ATAU slug kustom diisi.
$final_slug = $slug; // Default ke slug lama
if (!empty($slug_new_custom)) {
$potential_slug = generate_slug($slug_new_custom);
if ($potential_slug !== $slug) { // Hanya update jika slug kustom berbeda dari yg lama
$final_slug = $potential_slug;
}
} elseif ($title_new !== $title) { // Jika judul berubah dan tidak ada slug kustom
$final_slug = generate_slug($title_new);
}
// Cek keunikan slug BARU, JIKA slug berubah
if ($final_slug !== $slug) { // Hanya cek jika slug memang berubah
$sql_check_slug = "SELECT id FROM posts WHERE slug = ? AND id != ?"; // id != ? untuk mengecualikan post saat ini
if($stmt_check = mysqli_prepare($conn, $sql_check_slug)) {
mysqli_stmt_bind_param($stmt_check, "si", $final_slug, $post_id);
mysqli_stmt_execute($stmt_check);
mysqli_stmt_store_result($stmt_check);
if(mysqli_stmt_num_rows($stmt_check) > 0) {
$errors[] = "Slug '" . htmlspecialchars($final_slug) . "' sudah digunakan. Coba slug kustom lain atau ubah judul.";
}
mysqli_stmt_close($stmt_check);
}
}
if (empty($errors)) {
// Update data di database
$sql_update = "UPDATE posts SET title = ?, content = ?, slug = ?, status = ?, updated_at = NOW() WHERE id = ?";
if ($stmt_update = mysqli_prepare($conn, $sql_update)) {
mysqli_stmt_bind_param($stmt_update, "ssssi", $title_new, $content_new, $final_slug, $status_new, $post_id);
if (mysqli_stmt_execute($stmt_update)) {
set_flash_message('post_action', "Artikel \"" . htmlspecialchars($title_new) . "\" berhasil diperbarui!", "success");
header("Location: manage_posts.php");
exit;
} else {
$errors[] = "Gagal memperbarui artikel: " . mysqli_stmt_error($stmt_update);
}
mysqli_stmt_close($stmt_update);
} else {
$errors[] = "Gagal mempersiapkan statement UPDATE: " . mysqli_error($conn);
}
}
// Jika ada error, nilai $title, $content, $status, $slug akan diisi ulang dengan nilai BARU agar sticky
$title = $title_new;
$content = $content_new;
$status = $status_new;
$slug = $final_slug; // Gunakan slug yang telah diproses untuk ditampilkan kembali di form
}
// mysqli_close($conn); // Tutup di akhir
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo htmlspecialchars($page_title_admin); ?> - CMS Admin</title>
<link rel="stylesheet" href="../css/admin_style.css">
</head>
<body>
<header class="admin-header">
<h1>Admin Panel</h1>
<div>Selamat datang, <?php echo htmlspecialchars($current_username); ?> | <a href="logout.php">Logout</a></div>
</header>
<div class="admin-container">
<nav class="admin-nav">
<ul>
<li><a href="index.php">Dashboard</a></li>
<li><a href="manage_posts.php">Kelola Artikel</a></li>
<li><a href="manage_categories.php">Kelola Kategori</a></li>
<?php if ($current_user_role === 'admin'): ?>
<li><a href="manage_users.php">Kelola Pengguna</a></li>
<?php endif; ?>
</ul>
</nav>
<h2><?php echo htmlspecialchars($page_title_admin); ?>: "<?php echo htmlspecialchars($post['title']); // Judul asli artikel ?>"</h2>
<?php if (!empty($errors)): ?>
<div class="flash-message error">
<strong>Error!</strong>
<ul>
<?php foreach ($errors as $error): ?>
<li><?php echo htmlspecialchars($error); ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>?id=<?php echo $post_id; ?>" method="post" class="admin-form">
<div class="form-group">
<label for="title">Judul Artikel:</label>
<input type="text" name="title" id="title" value="<?php echo htmlspecialchars($title); ?>" required>
</div>
<div class="form-group">
<label for="slug">Slug (Opsional, akan diupdate otomatis jika judul berubah & ini kosong):</label>
<input type="text" name="slug" id="slug" value="<?php echo htmlspecialchars($slug); ?>" placeholder="contoh-slug-artikel-ini">
<small>Hanya huruf kecil, angka, dan strip (-). Jika kosong dan judul diubah, slug akan dibuat ulang. Jika diisi, akan menggunakan nilai ini (jika unik).</small>
</div>
<div class="form-group">
<label for="content">Konten:</label>
<textarea name="content" id="content" rows="15" required><?php echo htmlspecialchars($content); ?></textarea>
</div>
<div class="form-group">
<label for="status">Status:</label>
<select name="status" id="status">
<option value="published" <?php echo ($status === 'published') ? 'selected' : ''; ?>>Published</option>
<option value="draft" <?php echo ($status === 'draft') ? 'selected' : ''; ?>>Draft</option>
</select>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-submit">Simpan Perubahan</button>
<a href="manage_posts.php" class="btn btn-cancel">Batal</a>
</div>
</form>
</div>
<?php if(isset($conn)) { mysqli_close($conn); } ?>
</body>
</html>
Penjelasan admin/edit_post.php
:
- Mengambil ID Artikel:
(int)$_GET['id']
untuk memastikan ID adalah integer. - Mengambil Data Asli: Query SELECT untuk mengisi formulir dengan data artikel yang ada. Jika artikel tidak ditemukan, pengguna diarahkan kembali.
- Logika Update Slug: Slug hanya di-generate ulang atau diubah jika:
- Pengguna mengisi field slug kustom yang berbeda dari slug lama.
- Pengguna mengubah judul DAN field slug kustom kosong. Jika tidak, slug lama tetap digunakan.
- Pengecekan Keunikan Slug (jika berubah): Mirip seperti di
add_post.php
, tapi dengan kondisiAND id != ?
untuk mengecualikan artikel yang sedang diedit. - Prepared Statement untuk UPDATE.
- Menampilkan Judul Asli: Di atas form, judul asli artikel ditampilkan untuk referensi.
- Action Form: Menyertakan
?id=<?php echo $post_id; ?>
agar ID artikel tetap ada saat form disubmit.
Langkah 4: Skrip Hapus Artikel (admin/delete_post.php
)
Skrip ini akan menangani penghapusan artikel. Ini tidak memiliki tampilan HTML, hanya logika PHP.
Buat file admin/delete_post.php
:
<?php
// admin/delete_post.php
require_once 'auth_check.php';
require_once '../includes/db_connect.php';
require_once '../includes/functions.php';
// Ambil ID artikel dari URL dan pastikan itu integer
$post_id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($post_id > 0) {
// (Opsional) Cek dulu apakah artikel ada sebelum mencoba menghapus
$sql_check = "SELECT title FROM posts WHERE id = ?";
$title_to_delete = "Artikel"; // Default
if($stmt_check = mysqli_prepare($conn, $sql_check)) {
mysqli_stmt_bind_param($stmt_check, "i", $post_id);
mysqli_stmt_execute($stmt_check);
$result_check = mysqli_stmt_get_result($stmt_check);
if($row_check = mysqli_fetch_assoc($result_check)) {
$title_to_delete = $row_check['title'];
} else {
set_flash_message('post_action', "Gagal menghapus: Artikel dengan ID $post_id tidak ditemukan.", "error");
header("Location: manage_posts.php");
exit;
}
mysqli_stmt_close($stmt_check);
}
// Query untuk menghapus artikel
$sql_delete = "DELETE FROM posts WHERE id = ?";
if ($stmt_delete = mysqli_prepare($conn, $sql_delete)) {
mysqli_stmt_bind_param($stmt_delete, "i", $post_id);
if (mysqli_stmt_execute($stmt_delete)) {
// Cek apakah ada baris yang terpengaruh (benar-benar terhapus)
if (mysqli_stmt_affected_rows($stmt_delete) > 0) {
set_flash_message('post_action', "Artikel \"" . htmlspecialchars($title_to_delete) . "\" berhasil dihapus.", "success");
} else {
// Ini bisa terjadi jika ID tidak ada, meskipun kita sudah cek di atas
set_flash_message('post_action', "Gagal menghapus: Artikel tidak ditemukan atau sudah dihapus.", "error");
}
} else {
set_flash_message('post_action', "Gagal menghapus artikel: " . mysqli_stmt_error($stmt_delete), "error");
}
mysqli_stmt_close($stmt_delete);
} else {
set_flash_message('post_action', "Gagal mempersiapkan statement DELETE: " . mysqli_error($conn), "error");
}
} else {
set_flash_message('post_action', "ID Artikel tidak valid untuk dihapus.", "error");
}
// Selalu redirect kembali ke halaman manajemen artikel
header("Location: manage_posts.php");
exit;
// Tutup koneksi (meskipun exit akan menghentikan skrip)
// if(isset($conn)) { mysqli_close($conn); }
?>
Penjelasan admin/delete_post.php
:
- Tidak Ada Output HTML: Skrip ini hanya melakukan aksi dan redirect.
- Konfirmasi JavaScript: Sudah ada di
manage_posts.php
(onclick='return confirm(...)'
). Ini adalah konfirmasi sisi klien. Konfirmasi sisi server lebih aman tapi lebih kompleks untuk diimplementasikan di sini. - Prepared Statement untuk DELETE.
mysqli_stmt_affected_rows()
: Mengecek apakah ada baris yang benar-benar terhapus oleh query DELETE.- Flash Message dan Redirect: Memberikan umpan balik dan mengarahkan kembali ke daftar artikel.
Menguji Fitur Manajemen Artikel
- Buka
admin/manage_posts.php
: Anda akan melihat daftar artikel. - Tambah Artikel:
- Klik “Tambah Artikel Baru”.
- Isi formulir, coba biarkan slug kosong, lalu coba isi slug kustom.
- Coba buat error validasi (misalnya, judul kosong).
- Simpan artikel. Anda harus kembali ke
manage_posts.php
dengan pesan sukses dan artikel baru muncul.
- Edit Artikel:
- Klik “Edit” pada salah satu artikel.
- Formulir seharusnya terisi dengan data artikel tersebut.
- Ubah beberapa data, termasuk judul dan slug (coba buat slug yang sudah ada untuk melihat error).
- Simpan perubahan. Anda harus kembali ke
manage_posts.php
dengan pesan sukses.
- Hapus Artikel:
- Klik “Hapus” pada salah satu artikel.
- Konfirmasi JavaScript akan muncul.
- Jika Anda klik “OK”, artikel akan dihapus, dan Anda kembali ke
manage_posts.php
dengan pesan sukses.
Refleksi Part 3 dan Apa Selanjutnya?
Hebat! Anda telah berhasil mengimplementasikan fungsionalitas CRUD penuh untuk artikel di area admin CMS kita. Ini adalah inti dari kemampuan manajemen konten.
Poin Kunci yang Telah Dicapai:
- Halaman untuk menampilkan semua artikel di admin.
- Formulir untuk menambah dan mengedit artikel dengan validasi dasar.
- Pembuatan slug otomatis dan pengecekan keunikan slug.
- Logika untuk menghapus artikel dengan konfirmasi.
- Penggunaan flash messages untuk umpan balik pengguna.
- Penerapan prepared statements untuk semua operasi database.
Di Part 4 dari seri ini, kita akan melanjutkan dengan Manajemen Kategori. Kita akan membuat CRUD untuk kategori, dan kemudian mengintegrasikan kategori tersebut dengan sistem artikel kita, baik di frontend maupun di backend. Ini akan membuat CMS kita semakin terstruktur dan mudah dinavigasi.
Teruslah berlatih dengan memodifikasi dan mencoba berbagai skenario!