Sau khi nắm vững kết nối MySQL, bước tiếp theo là áp dụng tất cả kiến thức đã học vào một Mini Project CRUD hoàn chỉnh. CRUD (Create, Read, Update, Delete) là bộ 4 thao tác cơ bản nhất trong mọi ứng dụng web. Trong bài này, bạn sẽ xây dựng hệ thống quản lý sản phẩm với đầy đủ tính năng: thêm, sửa, xóa, tìm kiếm, phân trang và upload ảnh.
1. Giới thiệu Project
Tên project: Quản lý sản phẩm (Product Management)
Tính năng:
- ✅ Create: Thêm sản phẩm mới (tên, giá, mô tả, ảnh)
- ✅ Read: Hiển thị danh sách sản phẩm (có phân trang)
- ✅ Update: Sửa thông tin sản phẩm
- ✅ Delete: Xóa sản phẩm (có xác nhận)
- ✅ Search: Tìm kiếm theo tên
- ✅ Upload: Upload ảnh sản phẩm
- ✅ Validation: Kiểm tra dữ liệu đầu vào
- ✅ Security: Prepared Statements chống SQL Injection
2. Cấu trúc Project
product-management/
├── config/
│ └── database.php # Kết nối database
├── includes/
│ ├── header.php # Header chung
│ └── footer.php # Footer chung
├── uploads/ # Thư mục lưu ảnh
├── index.php # Trang danh sách sản phẩm
├── create.php # Thêm sản phẩm
├── edit.php # Sửa sản phẩm
├── delete.php # Xóa sản phẩm
├── search.php # Tìm kiếm
└── style.css # CSS đơn giản
3. Bước 1: Tạo Database
-- Tạo database
CREATE DATABASE product_management CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE product_management;
-- Tạo bảng products
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
description TEXT,
image VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Insert dữ liệu mẫu
INSERT INTO products (name, price, description, image) VALUES
('Laptop Dell XPS 13', 25000000, 'Laptop cao cấp, màn hình 13 inch, Intel Core i7', 'laptop-dell.jpg'),
('iPhone 15 Pro', 29000000, 'Smartphone flagship của Apple', 'iphone-15.jpg'),
('Tai nghe Sony WH-1000XM5', 8500000, 'Tai nghe chống ồn cao cấp', 'sony-headphone.jpg'),
('Bàn phím cơ Keychron K2', 2500000, 'Bàn phím cơ wireless, hot-swap switch', 'keyboard.jpg'),
('Chuột Logitech MX Master 3', 2200000, 'Chuột ergonomic cho dân văn phòng', 'mouse.jpg');
4. Bước 2: File config/database.php
<?php
class Database {
private $host = "localhost";
private $db_name = "product_management";
private $username = "root";
private $password = "";
public $conn;
// Kết nối database
public function getConnection() {
$this->conn = null;
try {
$this->conn = new PDO(
"mysql:host=" . $this->host . ";dbname=" . $this->db_name . ";charset=utf8mb4",
$this->username,
$this->password
);
// Đặt chế độ lỗi
$this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Đặt fetch mode mặc định
$this->conn->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch(PDOException $e) {
echo "Lỗi kết nối: " . $e->getMessage();
}
return $this->conn;
}
}
?>5. Bước 3: File includes/header.php
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $page_title ?? 'Quản lý sản phẩm' ?></title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<div class="container">
<h1>📦 Quản lý sản phẩm</h1>
<nav>
<a href="index.php">Danh sách</a>
<a href="create.php" class="btn-add">+ Thêm sản phẩm</a>
</nav>
</div>
</header>
<main class="container">6. Bước 4: File includes/footer.php
</main>
<footer>
<div class="container">
<p>© 2026 Quản lý sản phẩm - PHP CRUD Project</p>
</div>
</footer>
<script>
// Xác nhận xóa
function confirmDelete(id, name) {
if (confirm('Bạn có chắc muốn xóa sản phẩm "' + name + '"?')) {
window.location.href = 'delete.php?id=' + id;
}
}
</script>
</body>
</html>7. Bước 5: File index.php – Danh sách sản phẩm
<?php
$page_title = "Danh sách sản phẩm";
require_once 'config/database.php';
require_once 'includes/header.php';
// Kết nối database
$database = new Database();
$conn = $database->getConnection();
// Phân trang
$limit = 5; // Số sản phẩm mỗi trang
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$offset = ($page - 1) * $limit;
// Tìm kiếm
$search = isset($_GET['search']) ? $_GET['search'] : '';
$search_sql = '';
$params = [];
if ($search) {
$search_sql = "WHERE name LIKE :search";
$params[':search'] = "%$search%";
}
// Đếm tổng số sản phẩm
$count_sql = "SELECT COUNT(*) as total FROM products $search_sql";
$count_stmt = $conn->prepare($count_sql);
$count_stmt->execute($params);
$total_products = $count_stmt->fetch()['total'];
$total_pages = ceil($total_products / $limit);
// Lấy danh sách sản phẩm
$sql = "SELECT * FROM products $search_sql ORDER BY id DESC LIMIT :limit OFFSET :offset";
$stmt = $conn->prepare($sql);
// Bind parameters
if ($search) {
$stmt->bindParam(':search', $params[':search']);
}
$stmt->bindParam(':limit', $limit, PDO::PARAM_INT);
$stmt->bindParam(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$products = $stmt->fetchAll();
?>
<div class="search-box">
<form method="GET" action="index.php">
<input type="text" name="search" placeholder="Tìm kiếm sản phẩm..."
value="<?= htmlspecialchars($search) ?>">
<button type="submit">🔍 Tìm kiếm</button>
<?php if ($search): ?>
<a href="index.php" class="btn-clear">✕ Xóa</a>
<?php endif; ?>
</form>
</div>
<?php if (isset($_GET['msg'])): ?>
<div class="alert alert-success">
<?php
if ($_GET['msg'] == 'created') echo "✓ Thêm sản phẩm thành công!";
if ($_GET['msg'] == 'updated') echo "✓ Cập nhật sản phẩm thành công!";
if ($_GET['msg'] == 'deleted') echo "✓ Xóa sản phẩm thành công!";
?>
</div>
<?php endif; ?>
<div class="stats">
<p>Tổng số: <strong><?= $total_products ?></strong> sản phẩm</p>
</div>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Ảnh</th>
<th>Tên sản phẩm</th>
<th>Giá</th>
<th>Ngày tạo</th>
<th>Thao tác</th>
</tr>
</thead>
<tbody>
<?php if (count($products) > 0): ?>
<?php foreach ($products as $product): ?>
<tr>
<td><?= $product['id'] ?></td>
<td>
<?php if ($product['image']): ?>
<img src="uploads/<?= $product['image'] ?>" alt="<?= htmlspecialchars($product['name']) ?>" width="50">
<?php else: ?>
<span class="no-image">📷</span>
<?php endif; ?>
</td>
<td><strong><?= htmlspecialchars($product['name']) ?></strong></td>
<td><?= number_format($product['price'], 0, ',', '.') ?> VNĐ</td>
<td><?= date('d/m/Y', strtotime($product['created_at'])) ?></td>
<td class="actions">
<a href="edit.php?id=<?= $product['id'] ?>" class="btn btn-edit">✏️ Sửa</a>
<button onclick="confirmDelete(<?= $product['id'] ?>, '<?= htmlspecialchars($product['name']) ?>')"
class="btn btn-delete">🗑️ Xóa</button>
</td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="6" class="text-center">
<?= $search ? "Không tìm thấy sản phẩm phù hợp" : "Chưa có sản phẩm nào" ?>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
<!-- Phân trang -->
<?php if ($total_pages > 1): ?>
<div class="pagination">
<?php if ($page > 1): ?>
<a href="?page=<?= $page - 1 ?>&search=<?= urlencode($search) ?>">« Trước</a>
<?php endif; ?>
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
<a href="?page=<?= $i ?>&search=<?= urlencode($search) ?>"
class="<?= $i == $page ? 'active' : '' ?>">
<?= $i ?>
</a>
<?php endfor; ?>
<?php if ($page < $total_pages): ?>
<a href="?page=<?= $page + 1 ?>&search=<?= urlencode($search) ?>">Sau »</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php require_once 'includes/footer.php'; ?>8. Bước 6: File create.php - Thêm sản phẩm
<?php
$page_title = "Thêm sản phẩm";
require_once 'config/database.php';
require_once 'includes/header.php';
$database = new Database();
$conn = $database->getConnection();
$errors = [];
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$name = trim($_POST['name']);
$price = trim($_POST['price']);
$description = trim($_POST['description']);
// Validate
if (empty($name)) {
$errors['name'] = "Tên sản phẩm không được để trống";
} elseif (strlen($name) < 3) {
$errors['name'] = "Tên sản phẩm phải ít nhất 3 ký tự";
}
if (empty($price)) {
$errors['price'] = "Giá không được để trống";
} elseif (!is_numeric($price) || $price <= 0) {
$errors['price'] = "Giá phải là số dương";
}
// Upload ảnh
$image_name = '';
if (isset($_FILES['image']) && $_FILES['image']['error'] == 0) {
$allowed = ['jpg', 'jpeg', 'png', 'gif'];
$file_name = $_FILES['image']['name'];
$file_size = $_FILES['image']['size'];
$file_tmp = $_FILES['image']['tmp_name'];
$file_ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
if (!in_array($file_ext, $allowed)) {
$errors['image'] = "Chỉ cho phép upload: " . implode(", ", $allowed);
} elseif ($file_size > 2 * 1024 * 1024) {
$errors['image'] = "Kích thước file tối đa 2MB";
} else {
// Tạo thư mục uploads nếu chưa có
if (!is_dir('uploads')) {
mkdir('uploads', 0777, true);
}
// Đổi tên file
$image_name = uniqid() . '.' . $file_ext;
$upload_path = 'uploads/' . $image_name;
if (!move_uploaded_file($file_tmp, $upload_path)) {
$errors['image'] = "Lỗi khi upload file";
}
}
}
// Nếu không có lỗi, insert vào database
if (empty($errors)) {
try {
$sql = "INSERT INTO products (name, price, description, image) VALUES (:name, :price, :description, :image)";
$stmt = $conn->prepare($sql);
$stmt->execute([
':name' => $name,
':price' => $price,
':description' => $description,
':image' => $image_name
]);
header("Location: index.php?msg=created");
exit;
} catch (PDOException $e) {
$errors['db'] = "Lỗi: " . $e->getMessage();
}
}
}
?>
<h2>➕ Thêm sản phẩm mới</h2>
<?php if (isset($errors['db'])): ?>
<div class="alert alert-error"><?= $errors['db'] ?></div>
<?php endif; ?>
<form method="POST" enctype="multipart/form-data" class="form">
<div class="form-group">
<label>Tên sản phẩm: <span class="required">*</span></label>
<input type="text" name="name" value="<?= $_POST['name'] ?? '' ?>" required>
<?php if (isset($errors['name'])): ?>
<span class="error"><?= $errors['name'] ?></span>
<?php endif; ?>
</div>
<div class="form-group">
<label>Giá (VNĐ): <span class="required">*</span></label>
<input type="number" name="price" value="<?= $_POST['price'] ?? '' ?>" required>
<?php if (isset($errors['price'])): ?>
<span class="error"><?= $errors['price'] ?></span>
<?php endif; ?>
</div>
<div class="form-group">
<label>Mô tả:</label>
<textarea name="description" rows="4"><?= $_POST['description'] ?? '' ?></textarea>
</div>
<div class="form-group">
<label>Ảnh sản phẩm:</label>
<input type="file" name="image" accept="image/*">
<small>Cho phép: JPG, PNG, GIF. Tối đa: 2MB</small>
<?php if (isset($errors['image'])): ?>
<span class="error"><?= $errors['image'] ?></span>
<?php endif; ?>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">💾 Lưu sản phẩm</button>
<a href="index.php" class="btn btn-secondary">↩️ Hủy</a>
</div>
</form>
<?php require_once 'includes/footer.php'; ?>9. Bước 7: File edit.php - Sửa sản phẩm
<?php
$page_title = "Sửa sản phẩm";
require_once 'config/database.php';
require_once 'includes/header.php';
$database = new Database();
$conn = $database->getConnection();
$errors = [];
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id <= 0) {
header("Location: index.php");
exit;
}
// Lấy thông tin sản phẩm
$stmt = $conn->prepare("SELECT * FROM products WHERE id = ?");
$stmt->execute([$id]);
$product = $stmt->fetch();
if (!$product) {
header("Location: index.php");
exit;
}
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$name = trim($_POST['name']);
$price = trim($_POST['price']);
$description = trim($_POST['description']);
// Validate (giống create.php)
if (empty($name)) {
$errors['name'] = "Tên sản phẩm không được để trống";
} elseif (strlen($name) < 3) {
$errors['name'] = "Tên sản phẩm phải ít nhất 3 ký tự";
}
if (empty($price)) {
$errors['price'] = "Giá không được để trống";
} elseif (!is_numeric($price) || $price <= 0) {
$errors['price'] = "Giá phải là số dương";
}
// Upload ảnh mới (nếu có)
$image_name = $product['image']; // Giữ ảnh cũ
if (isset($_FILES['image']) && $_FILES['image']['error'] == 0) {
$allowed = ['jpg', 'jpeg', 'png', 'gif'];
$file_name = $_FILES['image']['name'];
$file_size = $_FILES['image']['size'];
$file_tmp = $_FILES['image']['tmp_name'];
$file_ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
if (!in_array($file_ext, $allowed)) {
$errors['image'] = "Chỉ cho phép upload: " . implode(", ", $allowed);
} elseif ($file_size > 2 * 1024 * 1024) {
$errors['image'] = "Kích thước file tối đa 2MB";
} else {
// Xóa ảnh cũ
if ($product['image'] && file_exists('uploads/' . $product['image'])) {
unlink('uploads/' . $product['image']);
}
// Upload ảnh mới
$image_name = uniqid() . '.' . $file_ext;
$upload_path = 'uploads/' . $image_name;
if (!move_uploaded_file($file_tmp, $upload_path)) {
$errors['image'] = "Lỗi khi upload file";
$image_name = $product['image']; // Giữ ảnh cũ nếu lỗi
}
}
}
// Update database
if (empty($errors)) {
try {
$sql = "UPDATE products SET name = :name, price = :price, description = :description, image = :image WHERE id = :id";
$stmt = $conn->prepare($sql);
$stmt->execute([
':name' => $name,
':price' => $price,
':description' => $description,
':image' => $image_name,
':id' => $id
]);
header("Location: index.php?msg=updated");
exit;
} catch (PDOException $e) {
$errors['db'] = "Lỗi: " . $e->getMessage();
}
}
}
?>
<h2>✏️ Sửa sản phẩm</h2>
<?php if (isset($errors['db'])): ?>
<div class="alert alert-error"><?= $errors['db'] ?></div>
<?php endif; ?>
<form method="POST" enctype="multipart/form-data" class="form">
<div class="form-group">
<label>Tên sản phẩm: <span class="required">*</span></label>
<input type="text" name="name" value="<?= htmlspecialchars($_POST['name'] ?? $product['name']) ?>" required>
<?php if (isset($errors['name'])): ?>
<span class="error"><?= $errors['name'] ?></span>
<?php endif; ?>
</div>
<div class="form-group">
<label>Giá (VNĐ): <span class="required">*</span></label>
<input type="number" name="price" value="<?= $_POST['price'] ?? $product['price'] ?>" required>
<?php if (isset($errors['price'])): ?>
<span class="error"><?= $errors['price'] ?></span>
<?php endif; ?>
</div>
<div class="form-group">
<label>Mô tả:</label>
<textarea name="description" rows="4"><?= htmlspecialchars($_POST['description'] ?? $product['description']) ?></textarea>
</div>
<div class="form-group">
<label>Ảnh sản phẩm:</label>
<?php if ($product['image']): ?>
<div class="current-image">
<img src="uploads/<?= $product['image'] ?>" alt="Ảnh hiện tại" width="150">
<p>Ảnh hiện tại</p>
</div>
<?php endif; ?>
<input type="file" name="image" accept="image/*">
<small>Bỏ trống nếu không muốn thay đổi ảnh</small>
<?php if (isset($errors['image'])): ?>
<span class="error"><?= $errors['image'] ?></span>
<?php endif; ?>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">💾 Cập nhật</button>
<a href="index.php" class="btn btn-secondary">↩️ Hủy</a>
</div>
</form>
<?php require_once 'includes/footer.php'; ?>10. Bước 8: File delete.php - Xóa sản phẩm
<?php
require_once 'config/database.php';
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id > 0) {
$database = new Database();
$conn = $database->getConnection();
// Lấy thông tin sản phẩm để xóa ảnh
$stmt = $conn->prepare("SELECT image FROM products WHERE id = ?");
$stmt->execute([$id]);
$product = $stmt->fetch();
if ($product) {
// Xóa ảnh nếu có
if ($product['image'] && file_exists('uploads/' . $product['image'])) {
unlink('uploads/' . $product['image']);
}
// Xóa sản phẩm
$stmt = $conn->prepare("DELETE FROM products WHERE id = ?");
$stmt->execute([$id]);
header("Location: index.php?msg=deleted");
exit;
}
}
header("Location: index.php");
exit;
?>11. Bước 9: File style.css - CSS đơn giản
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f5f5f5;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
/* Header */
header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px 0;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
header .container {
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 {
font-size: 24px;
}
header nav a {
color: white;
text-decoration: none;
margin-left: 20px;
padding: 8px 16px;
border-radius: 5px;
transition: background 0.3s;
}
header nav a:hover {
background: rgba(255,255,255,0.2);
}
.btn-add {
background: #4CAF50;
font-weight: bold;
}
/* Main */
main {
padding: 30px 0;
min-height: calc(100vh - 180px);
}
/* Search */
.search-box {
background: white;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.search-box form {
display: flex;
gap: 10px;
}
.search-box input {
flex: 1;
padding: 10px 15px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 14px;
}
.search-box button {
padding: 10px 20px;
background: #667eea;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
}
.btn-clear {
padding: 10px 20px;
background: #dc3545;
color: white;
text-decoration: none;
border-radius: 5px;
font-size: 14px;
}
/* Alert */
.alert {
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
/* Stats */
.stats {
background: white;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
/* Table */
.table {
width: 100%;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.table thead {
background: #667eea;
color: white;
}
.table th, .table td {
padding: 15px;
text-align: left;
}
.table tbody tr:hover {
background: #f8f9fa;
}
.table tbody tr:not(:last-child) {
border-bottom: 1px solid #dee2e6;
}
.no-image {
font-size: 30px;
display: inline-block;
}
.text-center {
text-align: center;
padding: 40px;
color: #999;
}
/* Actions */
.actions {
display: flex;
gap: 10px;
}
.btn {
padding: 8px 15px;
text-decoration: none;
border-radius: 5px;
font-size: 13px;
border: none;
cursor: pointer;
transition: opacity 0.3s;
}
.btn:hover {
opacity: 0.8;
}
.btn-edit {
background: #ffc107;
color: #333;
}
.btn-delete {
background: #dc3545;
color: white;
}
/* Pagination */
.pagination {
display: flex;
justify-content: center;
gap: 5px;
margin-top: 20px;
}
.pagination a {
padding: 8px 12px;
background: white;
color: #667eea;
text-decoration: none;
border-radius: 5px;
border: 2px solid #667eea;
transition: all 0.3s;
}
.pagination a:hover,
.pagination a.active {
background: #667eea;
color: white;
}
/* Form */
.form {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
max-width: 600px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 10px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 14px;
}
.form-group textarea {
resize: vertical;
}
.form-group small {
display: block;
margin-top: 5px;
color: #666;
font-size: 12px;
}
.required {
color: red;
}
.error {
color: red;
font-size: 13px;
display: block;
margin-top: 5px;
}
.current-image {
margin-bottom: 15px;
}
.current-image img {
border: 2px solid #ddd;
border-radius: 5px;
padding: 5px;
background: white;
}
.form-actions {
display: flex;
gap: 10px;
margin-top: 30px;
}
.btn-primary {
background: #667eea;
color: white;
padding: 12px 30px;
}
.btn-secondary {
background: #6c757d;
color: white;
padding: 12px 30px;
}
/* Footer */
footer {
background: #333;
color: white;
text-align: center;
padding: 20px 0;
margin-top: 40px;
}
Tóm tắt
Qua bài này bạn đã xây dựng được:
- ✅ CRUD hoàn chỉnh: Create, Read, Update, Delete với PHP MySQL
- ✅ Phân trang: Hiển thị 5 sản phẩm/trang với LIMIT và OFFSET
- ✅ Tìm kiếm: Tìm theo tên sản phẩm với LIKE
- ✅ Upload ảnh: Validate extension/size, đổi tên file, xóa ảnh cũ
- ✅ Validation: Kiểm tra dữ liệu đầu vào (name, price, image)
- ✅ Security: Prepared Statements chống SQL Injection
- ✅ UX: Xác nhận xóa, thông báo thành công, giữ giá trị form
- ✅ CSS: Giao diện đẹp, responsive
Các kỹ năng đã áp dụng:
- ✅ Kết nối MySQL với PDO
- ✅ Prepared Statements
- ✅ Form processing & validation
- ✅ File upload
- ✅ Pagination
- ✅ Search functionality
- ✅ Session & redirect
Bạn có thể mở rộng project với: đăng nhập/đăng ký, phân quyền user, category, filter theo giá, soft delete...
🎯 Bài tập mở rộng
- Bài 1: Thêm tính năng category (danh mục sản phẩm). Tạo bảng categories, thêm category_id vào products, hiển thị dropdown trong form.
- Bài 2: Thêm filter theo giá: Dưới 5 triệu, 5-10 triệu, 10-20 triệu, Trên 20 triệu.
- Bài 3: Thêm soft delete: Không xóa thật mà chỉ đánh dấu deleted_at. Tạo trang "Thùng rác" để khôi phục.
Gợi ý Bài 1 - Thêm Category:
-- Tạo bảng categories
CREATE TABLE categories (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
INSERT INTO categories (name) VALUES
('Laptop'), ('Điện thoại'), ('Phụ kiện');
-- Thêm cột category_id vào products
ALTER TABLE products ADD category_id INT AFTER name;
ALTER TABLE products ADD FOREIGN KEY (category_id) REFERENCES categories(id);
<?php
// Trong form create/edit, hiển thị dropdown
$categories = $conn->query("SELECT * FROM categories")->fetchAll();
?>
<select name="category_id" required>
<option value="">Chọn danh mục</option>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>"><?= $cat['name'] ?></option>
<?php endforeach; ?>
</select>