Bài này nối form HTML với CSDL: nhận dữ liệu sinh viên từ form, kiểm tra hợp lệ và thêm vào bảng bằng prepared statement. Đây là kỹ thuật quan trọng nhất của loạt bài vì nó ngăn chặn tận gốc SQL Injection.
- Hiểu bốn bước prepare → bind_param → execute → close
- Thêm bản ghi từ dữ liệu form an toàn
- Lấy id vừa thêm, xử lý lỗi trùng khóa
- Thêm nhiều bản ghi với một statement, dùng transaction
- Placeholder
- Dấu ? trong câu SQL giữ chỗ cho giá trị.
- bind_param
- Gắn biến PHP vào placeholder kèm kiểu: s, i, d, b.
- insert_id
- id tự tăng của bản ghi vừa thêm.
- Duplicate entry
- Lỗi trùng khóa UNIQUE/PRIMARY (mã 1062).
Khái niệm và lý thuyết
Thay vì ghép giá trị vào chuỗi SQL, ta gửi mẫu có dấu ? rồi gửi giá trị riêng. MySQL biên dịch mẫu một lần và coi giá trị là dữ liệu thuần, nên chuỗi như '; DROP TABLE sinhvien; -- chỉ là một cái tên kỳ lạ, không phải lệnh. Xem loạt SQL, Bài 10 để hiểu cơ chế tấn công.
| Ký tự kiểu | Kiểu PHP |
|---|---|
| s | string (chuỗi, ngày dạng chuỗi) |
| i | integer |
| d | double/float |
| b | blob (nhị phân) |
Form thêm sinh viên
<?php
// them.php
require_once __DIR__ . "/db_connect.php";
function e($s) { return htmlspecialchars((string)$s, ENT_QUOTES, "UTF-8"); }
$loi = []; $d = ["ma_sv" => "", "ho_ten" => "", "lop" => "", "ngay_sinh" => "", "email" => ""];
if ($_SERVER["REQUEST_METHOD"] === "POST") {
foreach ($d as $k => $_) $d[$k] = trim($_POST[$k] ?? "");
if (!preg_match('/^SV\d{3,8}$/', $d["ma_sv"])) $loi["ma_sv"] = "Mã dạng SV + số";
if (mb_strlen($d["ho_ten"]) < 3) $loi["ho_ten"] = "Họ tên quá ngắn";
if ($d["email"] !== "" && !filter_var($d["email"], FILTER_VALIDATE_EMAIL)) $loi["email"] = "Email sai";
$ns = $d["ngay_sinh"] !== "" ? $d["ngay_sinh"] : null; // input type=date gửi Y-m-d
if (!$loi) {
$sql = "INSERT INTO sinhvien (ma_sv, ho_ten, lop, ngay_sinh, email) VALUES (?, ?, ?, ?, ?)";
$stmt = $conn->prepare($sql);
$stmt->bind_param("sssss", $d["ma_sv"], $d["ho_ten"], $d["lop"], $ns, $d["email"]);
try {
$stmt->execute();
header("Location: danh-sach.php?them=" . $stmt->insert_id, true, 303);
exit;
} catch (mysqli_sql_exception $ex) {
$loi["chung"] = $ex->getCode() === 1062 ? "Mã sinh viên đã tồn tại" : "Không lưu được dữ liệu";
} finally {
$stmt->close();
}
}
}
?>
<form method="post">
<p style="color:red"><?= e($loi["chung"] ?? "") ?></p>
<p>Mã SV <input name="ma_sv" value="<?= e($d["ma_sv"]) ?>" required> <small><?= e($loi["ma_sv"] ?? "") ?></small></p>
<p>Họ tên <input name="ho_ten" value="<?= e($d["ho_ten"]) ?>" required> <small><?= e($loi["ho_ten"] ?? "") ?></small></p>
<p>Lớp <input name="lop" value="<?= e($d["lop"]) ?>"></p>
<p>Ngày sinh <input type="date" name="ngay_sinh" value="<?= e($d["ngay_sinh"]) ?>"></p>
<p>Email <input type="email" name="email" value="<?= e($d["email"]) ?>"> <small><?= e($loi["email"] ?? "") ?></small></p>
<button>Thêm sinh viên</button>
</form>Thêm nhiều bản ghi và transaction
<?php
$ds = [["SV101", "Lò Văn Khánh", "K66 Tin"], ["SV102", "Cầm Thị Liên", "K66 Tin"]];
$stmt = $conn->prepare("INSERT INTO sinhvien (ma_sv, ho_ten, lop) VALUES (?, ?, ?)");
$stmt->bind_param("sss", $ma, $ten, $lop); // gắn biến một lần
$conn->begin_transaction();
try {
foreach ($ds as [$ma, $ten, $lop]) $stmt->execute(); // đổi giá trị biến rồi execute lại
$conn->commit();
echo "Đã thêm " . count($ds) . " sinh viên";
} catch (mysqli_sql_exception $e) {
$conn->rollback(); // một dòng lỗi: hủy tất cả
echo "Lỗi, không thêm dòng nào";
}Mẫu này dùng để nhập điểm cả lớp hoặc nhập sinh viên từ tệp CSV (kết hợp fgetcsv ở loạt PHP, Bài 13).
Cùng việc bằng PDO
<?php
$st = $pdo->prepare("INSERT INTO sinhvien (ma_sv, ho_ten, lop) VALUES (:ma, :ten, :lop)");
$st->execute([":ma" => $ma, ":ten" => $ten, ":lop" => $lop]);
echo $pdo->lastInsertId();Sau khi thêm thành công, chuyển hướng (PRG) sang trang danh sách kèm thông báo; người dùng bấm F5 sẽ không thêm trùng bản ghi.
Không đặt nháy quanh dấu ?: VALUES ('?') là chuỗi chứa dấu hỏi chứ không phải placeholder, lỗi rất hay gặp của người mới.
- Hoàn thiện them.php với đầy đủ kiểm tra lỗi.
- Thử nhập tên chứa dấu nháy (O'Brien) và chuỗi tấn công, kiểm tra dữ liệu lưu đúng.
- Viết trang nhập điểm: chọn môn, nhập điểm cho 5 sinh viên, lưu bằng một transaction.
- Nhập danh sách sinh viên từ tệp CSV vào CSDL.
- prepare → bind_param → execute → close.
- Kiểu s, i, d, b; không đặt nháy quanh ?.
- insert_id lấy id mới; mã lỗi 1062 là trùng khóa.
- Transaction + một statement cho nhiều bản ghi.