Hiển thị danh sách là chức năng có trong mọi ứng dụng. Bài này học đọc kết quả SELECT bằng fetch_assoc, fetch_all, hiển thị thành bảng HTML, xem chi tiết một bản ghi, tìm kiếm, lọc và phân trang an toàn.
- Thực hiện SELECT và duyệt kết quả
- Hiển thị bảng HTML an toàn với htmlspecialchars
- Lấy một bản ghi theo id/mã bằng prepared statement
- Xây dựng tìm kiếm, lọc, phân trang
- Result set
- Tập kết quả trả về từ SELECT.
- fetch_assoc
- Lấy một dòng dạng mảng kết hợp.
- num_rows
- Số dòng trong kết quả.
- Phân trang
- Chia kết quả thành nhiều trang bằng LIMIT/OFFSET.
Khái niệm và lý thuyết
Khác INSERT/UPDATE/DELETE chỉ trả true/false, SELECT trả về một result set, ta duyệt từng dòng. Không có dữ liệu động (tham số) thì dùng $conn->query(); có dữ liệu từ người dùng thì bắt buộc dùng prepared statement + get_result().
| Phương thức | Trả về |
|---|---|
| fetch_assoc() | Một dòng: ["ma_sv" => ..., ...] hoặc null |
| fetch_all(MYSQLI_ASSOC) | Mọi dòng thành mảng |
| fetch_object() | Một dòng dạng đối tượng $r->ho_ten |
| num_rows | Số dòng |
Danh sách sinh viên
<?php
// danh-sach.php
require_once __DIR__ . "/db_connect.php";
function e($s) { return htmlspecialchars((string)$s, ENT_QUOTES, "UTF-8"); }
$result = $conn->query("SELECT id, ma_sv, ho_ten, lop, ngay_sinh, email FROM sinhvien ORDER BY ho_ten");
?>
<p>Tổng: <?= $result->num_rows ?> sinh viên | <a href="/them.php">+ Thêm</a></p>
<table border="1" cellpadding="6">
<tr><th>STT</th><th>Mã SV</th><th>Họ tên</th><th>Lớp</th><th>Ngày sinh</th><th>Thao tác</th></tr>
<?php $stt = 0; while ($r = $result->fetch_assoc()): ?>
<tr>
<td><?= ++$stt ?></td>
<td><?= e($r["ma_sv"]) ?></td>
<td><a href="/chi-tiet.php?id=<?= (int)$r["id"] ?>"><?= e($r["ho_ten"]) ?></a></td>
<td><?= e($r["lop"]) ?></td>
<td><?= $r["ngay_sinh"] ? date("d/m/Y", strtotime($r["ngay_sinh"])) : "" ?></td>
<td><a href="/sua.php?id=<?= (int)$r["id"] ?>">Sửa</a></td>
</tr>
<?php endwhile; ?>
<?php if ($result->num_rows === 0): ?><tr><td colspan="6">Chưa có dữ liệu</td></tr><?php endif; ?>
</table>Chi tiết một bản ghi
<?php
// chi-tiet.php?id=3
require_once __DIR__ . "/db_connect.php";
$id = filter_input(INPUT_GET, "id", FILTER_VALIDATE_INT);
if (!$id) { http_response_code(400); exit("Thiếu mã"); }
$stmt = $conn->prepare("SELECT * FROM sinhvien WHERE id = ?");
$stmt->bind_param("i", $id);
$stmt->execute();
$sv = $stmt->get_result()->fetch_assoc();
if (!$sv) { http_response_code(404); exit("Không tìm thấy sinh viên"); }
?>
<h1><?= htmlspecialchars($sv["ho_ten"]) ?></h1>
<p>Mã: <?= htmlspecialchars($sv["ma_sv"]) ?> — Lớp: <?= htmlspecialchars($sv["lop"]) ?></p>Tìm kiếm, lọc và phân trang
<?php
$q = trim($_GET["q"] ?? "");
$lop = trim($_GET["lop"] ?? "");
$page = max(1, (int)($_GET["page"] ?? 1));
$limit = 10; $offset = ($page - 1) * $limit;
$where = "WHERE (ho_ten LIKE ? OR ma_sv LIKE ?)"; $types = "ss"; $params = ["%$q%", "%$q%"];
if ($lop !== "") { $where .= " AND lop = ?"; $types .= "s"; $params[] = $lop; }
// Đếm tổng để tính số trang
$st = $conn->prepare("SELECT COUNT(*) FROM sinhvien $where");
$st->bind_param($types, ...$params); $st->execute();
$tong = $st->get_result()->fetch_row()[0];
$so_trang = max(1, (int)ceil($tong / $limit));
// Lấy dữ liệu trang hiện tại
$st = $conn->prepare("SELECT * FROM sinhvien $where ORDER BY ho_ten LIMIT ? OFFSET ?");
$st->bind_param($types . "ii", ...[...$params, $limit, $offset]); $st->execute();
$ds = $st->get_result()->fetch_all(MYSQLI_ASSOC);
?>
<form method="get">
<input name="q" value="<?= htmlspecialchars($q) ?>" placeholder="Tên hoặc mã">
<select name="lop"><option value="">Tất cả lớp</option>
<?php foreach (["K65 Tin", "K66 Tin"] as $l): ?>
<option <?= $l === $lop ? "selected" : "" ?>><?= $l ?></option><?php endforeach; ?></select>
<button>Tìm</button>
</form>
<p>Tìm thấy <?= $tong ?> kết quả</p>
<!-- bảng $ds như trên -->
<nav>
<?php for ($i = 1; $i <= $so_trang; $i++): ?>
<?php $url = "?" . http_build_query(["q" => $q, "lop" => $lop, "page" => $i]); ?>
<?= $i === $page ? "<b>$i</b>" : "<a href='" . htmlspecialchars($url) . "'>$i</a>" ?>
<?php endfor; ?>
</nav>http_build_query giữ lại từ khóa và bộ lọc khi chuyển trang. Kết hợp JOIN (loạt SQL, Bài 6) để hiển thị tên lớp từ bảng lop.
Với bảng lớn, chỉ SELECT các cột cần hiển thị thay vì *, và tạo chỉ mục cho cột tìm kiếm (loạt SQL, Bài 8).
Mọi giá trị in ra HTML phải qua htmlspecialchars, kể cả dữ liệu “của mình” trong CSDL: chúng có thể đã bị nhập mã độc từ trước.
- Hiển thị danh sách sinh viên kèm tên lớp (JOIN bảng lop).
- Thêm tìm kiếm theo tên/mã và lọc theo lớp.
- Phân trang 5 dòng mỗi trang, có nút Trước/Sau.
- Làm trang chi tiết hiển thị cả bảng điểm của sinh viên.
- query() khi không có tham số; prepare + get_result() khi có.
- fetch_assoc trong while, hoặc fetch_all.
- Chi tiết: lọc id bằng FILTER_VALIDATE_INT, 404 khi không có.
- Tìm kiếm LIKE ? + phân trang LIMIT ? OFFSET ?, đếm tổng bằng COUNT(*).