EzCode # Bài 11 — Danh sách khóa học
Học xong bài này bạn sẽ:
- Viết được Controller dùng ba Model cùng lúc để dựng một trang
- Tạo
Models/Category.phpvà thêm phương thức đầu tiên vàoModels/User.php- Có trang Khóa học với thanh lọc bên trái và lưới khóa học bên phải
Cần học trước: Bài 10
1. Vấn đề đặt ra
Trang chủ bạn làm ở Bài 10 chỉ liệt kê khóa học. Đơn giản: một Controller, một Model, một View.
Trang Khóa học phức tạp hơn. Nhìn vào giao diện, bạn thấy nó cần ba nguồn dữ liệu khác nhau:
| Vùng trên trang | Dữ liệu cần | Lấy từ bảng nào |
|---|---|---|
| Lưới khóa học bên phải | Danh sách khóa học | courses |
| Nhóm lọc "Tên giảng viên" | Danh sách giảng viên | users với role = 'teacher' |
| Nhóm lọc "Danh mục" | 33 danh mục | categories |
Ba bảng, ba Model. Câu hỏi đặt ra: một Controller có được dùng nhiều Model không?
Câu trả lời là có, và đó chính là việc của Controller. Bài này cho bạn thấy khuôn mẫu đó.
2. Kiến thức mới
2.1. Một Controller, nhiều Model
Nguyên tắc phân tầng ở Bài 9 nói: mỗi Model chỉ lo một bảng. Courses lo bảng courses, User lo bảng users, Category lo bảng categories.
Nhưng một trang web thường cần dữ liệu từ nhiều bảng. Ai ghép chúng lại? Controller.
public function list()
{
include_once "./Models/Courses.php";
$coursesModel = new Courses();
$coursesList = $coursesModel->getAll();
include_once "./Models/User.php";
$userModel = new User();
$teachersList = $userModel->getAllTeacher();
include_once "./Models/Category.php";
$categoryModel = new Category();
$categoriesList = $categoryModel->getAll();
include_once "./Views/course_list.php";
}
Ba khối giống hệt nhau, mỗi khối ba dòng: nạp file, tạo object, gọi phương thức. Rồi cuối cùng nạp một View duy nhất, và View đó thấy được cả ba biến nhờ cơ chế cùng phạm vi biến ở Bài 10 mục 2.3.
Đây là khuôn mẫu bạn sẽ lặp lại rất nhiều. Mọi Controller phức tạp của EzCode đều có hình dạng này.
2.2. Quy ước đặt tên biến
EzCode dùng quy ước rất nhất quán, và hiểu nó giúp bạn đọc code nhanh hơn:
| Loại | Cách đặt | Ví dụ |
|---|---|---|
| Object Model | $ten + Model |
$coursesModel, $userModel, $categoryModel |
| Mảng nhiều dòng | Số nhiều + List |
$coursesList, $teachersList, $categoriesList |
| Một dòng đơn lẻ | Số ít | $course, $user, $teacher |
Nhờ vậy khi đọc $teachersList bạn biết ngay đó là mảng lồng nhau, còn $teacher là một mảng kết hợp.
Quy ước không phải luật, nhưng phá vỡ nó làm code khó đọc. Hãy giữ đúng khi bạn thêm chức năng mới.
2.3. Truyền tham số cố định vào truy vấn
function getAllTeacher()
{
$sql = "SELECT * FROM users WHERE role=?";
return $this->db->query($sql, 'teacher');
}
Chuỗi 'teacher' không đến từ người dùng — nó do lập trình viên viết cứng. Về mặt bảo mật, nối thẳng vào SQL cũng không sao:
$sql = "SELECT * FROM users WHERE role='teacher'"; // cũng an toàn
Nhưng tác giả vẫn dùng ?. Vì sao?
Vì tính nhất quán. Khi mọi truy vấn trong project đều dùng ?, bạn không bao giờ phải dừng lại tự hỏi "chỗ này có an toàn không". Quy tắc đơn giản luôn dễ tuân thủ hơn quy tắc có ngoại lệ.
Đây là một thói quen tốt đáng học theo.
2.4. Đọc tham số $_GET với giá trị mặc định
$keyword = isset($_GET['keyword']) ? $_GET['keyword'] : '';
$teachers = isset($_GET['teacher']) ? $_GET['teacher'] : [];
Cú pháp điều_kiện ? giá_trị_nếu_đúng : giá_trị_nếu_sai gọi là toán tử ba ngôi. Nó là dạng viết gọn của if/else khi chỉ cần chọn giữa hai giá trị.
Bốn dòng trên tương đương với:
if (isset($_GET['keyword'])) {
$keyword = $_GET['keyword'];
} else {
$keyword = '';
}
So sánh với toán tử ?? mà bạn học ở Bài 4:
$keyword = $_GET['keyword'] ?? ''; // ngắn hơn, cùng ý nghĩa
Hai cách gần như tương đương. Khác biệt duy nhất: ?? chỉ kiểm tra "tồn tại và khác null", còn isset() ? : cũng vậy. Trong ngữ cảnh này chúng hoàn toàn thay thế được cho nhau.
EzCode dùng cả hai kiểu ở các file khác nhau — CourseController dùng ba ngôi, UserController dùng ??. Đây là sự thiếu nhất quán về phong cách, không phải lỗi.
Chú ý giá trị mặc định của $teachers là mảng rỗng [], không phải chuỗi rỗng. Vì như Bài 4 mục 2.4 đã nói, ô name="teacher[]" cho ra một mảng. Nếu người dùng không tick ô nào thì khóa teacher không tồn tại, và ta cần một mảng rỗng để code phía sau không lỗi.
2.5. Form GET giữ lại lựa chọn của người dùng
Sau khi người dùng bấm "Áp dụng bộ lọc", trang tải lại. Nếu form trở về trạng thái trống, người dùng sẽ không biết mình đang lọc theo gì.
Ba loại ô cần ba cách xử lý khác nhau:
Ô nhập chữ — điền lại bằng value:
<input type="text" name="keyword" value="<?= isset($_GET['keyword']) ? htmlspecialchars($_GET['keyword']) : '' ?>" />
Ô tick chọn — thêm thuộc tính checked:
<input type="checkbox" name="teacher[]" value="<?= $teacher['id'] ?>"
<?= (isset($_GET['teacher']) && in_array($teacher['id'], $_GET['teacher'])) ? 'checked' : '' ?> />
in_array($kim, $dongCoKho) trả về true nếu $kim có trong mảng. Ở đây nó hỏi: "giảng viên này có nằm trong danh sách người dùng đã tick không?"
Ô chọn một — thêm thuộc tính selected:
<option value="price_asc" <?= (isset($_GET['sort']) && $_GET['sort'] == 'price_asc') ? 'selected' : '' ?>>Giá tăng dần</option>
Cả ba đều dùng chung ý tưởng: so giá trị của ô với giá trị trong $_GET, khớp thì in thêm một từ khóa HTML.
2.6. Ô ẩn để giữ tham số điều hướng
Đây là một chi tiết nhỏ nhưng quan trọng.
Form của trang lọc dùng method="GET". Khi bấm nút gửi, trình duyệt xóa sạch chuỗi truy vấn cũ và tạo lại từ các ô trong form.
Nghĩa là ?ctrl=course&act=list sẽ biến mất, và địa chỉ mới chỉ còn ?keyword=Python&sort=newest. Không còn ctrl và act, index.php sẽ rơi vào nhánh else và mở trang chủ.
Giải pháp là nhét chúng vào form dưới dạng ô ẩn:
<input type="hidden" name="ctrl" value="course" />
<input type="hidden" name="act" value="list" />
Ô type="hidden" không hiện ra màn hình nhưng vẫn được gửi đi như ô thường. Nhờ vậy địa chỉ sau khi lọc đầy đủ:
?ctrl=course&act=list&keyword=Python&teacher%5B%5D=35&sort=newest
Chuỗi
%5B%5Dlà mã hóa URL của cặp ngoặc vuông[]. Trình duyệt tự mã hóa, PHP tự giải mã. Bạn không cần làm gì.
3. Áp dụng vào EzCode
Bài này tạo 3 file mới và 1 View. Làm theo thứ tự.
3.1. Tạo Model Category
Đây là file bạn đã đọc kỹ ở Bài 6. Giờ tạo thật.
📁
Models/Category.php— tạo mới
<?php
include_once "./Models/Database.php";
class Category
{
private $db;
function __construct()
{
$this->db = new Database();
}
function getAll()
{
$sql = "SELECT * FROM categories";
return $this->db->query($sql);
}
}
Model đơn giản nhất project: một phương thức, không tham số, không điều kiện. Nó trả về cả 33 danh mục.
Chú ý câu SQL không có ORDER BY. Nghĩa là thứ tự trả về do MySQL tự quyết định — thường là theo thứ tự chèn vào, nhưng không có gì bảo đảm. Muốn chắc chắn theo bảng chữ cái thì phải thêm ORDER BY name ASC.
3.2. Tạo Model User
📁
Models/User.php— tạo mới
<?php
include_once "./Models/Database.php";
class User
{
private $db;
function __construct()
{
$this->db = new Database();
}
function getAllTeacher()
{
$sql = "SELECT * FROM users WHERE role=?";
return $this->db->query($sql, 'teacher');
}
Chú ý: khối trên chưa đóng class. Thêm một dòng
}ở cuối file.Model
Usersẽ lớn dần qua 4 bài nữa: Bài 14 thêmgetById,getByEmail,create; Bài 16 thêmupdateProfile,updatePassword,getEnrolledCourses; Bài 19 thêm hai phương thức thống kê học viên; Bài 21 thêmcheckEnrollmentvàenrollCourse. Cuối cùng nó có 10 phương thức.Mỗi lần thêm, bạn chèn phương thức mới vào trước dấu
}cuối cùng.
Câu truy vấn SELECT * FROM users WHERE role=? với tham số 'teacher' sẽ trả về 12 giảng viên trong dữ liệu mẫu.
Một điểm cần biết: câu này dùng
SELECT *, nghĩa là nó lấy cả cộtpasswordvề. Trang lọc chỉ cầnidvàname, nhưng chuỗi băm mật khẩu của 12 giảng viên vẫn được đưa vào bộ nhớ PHP.Không phải lỗ hổng trực tiếp — dữ liệu không được in ra HTML. Nhưng đây là thói quen kém: nếu sau này ai đó thêm một dòng
print_r($teachersList)để gỡ lỗi và quên xóa, toàn bộ chuỗi băm sẽ lộ ra trang.Cách tốt hơn là chỉ lấy cột cần dùng:
SELECT id, name FROM users WHERE role=?. Bạn cứ gõ theo repo, và ghi nhớ nguyên tắc này.
3.3. Tạo Controller
Tạo thư mục Controllers nếu chưa có, rồi tạo file.
📁
Controllers/CourseController.php— tạo mới
<?php
class CourseController
{
public function list()
{
// Lấy các tham số filter từ $_GET
$keyword = isset($_GET['keyword']) ? $_GET['keyword'] : '';
$teachers = isset($_GET['teacher']) ? $_GET['teacher'] : [];
$categories = isset($_GET['category']) ? $_GET['category'] : [];
$sort = isset($_GET['sort']) ? $_GET['sort'] : 'newest';
include_once "./Models/Courses.php"; // Include the Courses model
$coursesModel = new Courses(); // Create an instance of the Courses model
$coursesList = $coursesModel->filter($keyword, $teachers, $categories, $sort); // Fetch filtered courses
include_once "./Models/User.php";
$userModel = new User(); // Create an instance of the User model
$teachersList = $userModel->getAllTeacher(); // Fetch all teachers
include_once "./Models/Category.php";
$categoryModel = new Category(); // Create an instance of the Category model
$categoriesList = $categoryModel->getAll(); // Fetch all categories
// Assuming course_list.php is the view for listing courses
include_once "./Views/course_list.php";
Chú ý: khối trên chưa đóng phương thức và chưa đóng class. Thêm hai dòng ở cuối file:
} }Bài 13 sẽ thêm phương thức
detail($id)và Bài 21 thêmregister($id), cả hai chèn vào trước dấu}cuối cùng.
Quan trọng — bạn phải sửa tạm một dòng để trang chạy được.
Dòng gọi
$coursesModel->filter(...)tham chiếu tới một phương thức chưa tồn tại trongModels/Courses.php. Bạn mới chỉ viếtgetAll()ở Bài 10.Chạy ngay bây giờ sẽ nhận:
Fatal error: Uncaught Error: Call to undefined method Courses::filter()Hãy tạm đổi dòng đó thành:
$coursesList = $coursesModel->getAll(); // TẠM THỜI - Bài 12 sẽ đổi lạiBài 12 sẽ viết
filter()và bạn trả lại nguyên bản. Đây là cách duy nhất để mỗi bài đều có sản phẩm chạy được mà vẫn giữ code khớp với repo ở bài cuối.
Bốn dòng đầu của phương thức đọc tham số lọc từ URL, dùng toán tử ba ngôi ở mục 2.4. Bài này chúng chưa có tác dụng gì vì getAll() không nhận tham số. Bài 12 sẽ dùng tới cả bốn.
Ba khối tiếp theo là khuôn mẫu ở mục 2.1: nạp Model, tạo object, lấy dữ liệu — lặp lại ba lần cho ba Model.
3.4. Tạo View
📁
Views/course_list.php— tạo mới
<main class="course-page">
<form method="GET" class="sidebar">
<input type="hidden" name="ctrl" value="course" />
<input type="hidden" name="act" value="list" />
<h3>Lọc khóa học</h3>
<input type="text" placeholder="Tìm theo tên..." class="search-bar" name="keyword" value="<?= isset($_GET['keyword']) ? htmlspecialchars($_GET['keyword']) : '' ?>" />
<div class="filter-group">
<h4>Tên giảng viên</h4>
<?php foreach ($teachersList as $teacher): ?>
<label><input type="checkbox" name="teacher[]" value="<?= $teacher['id'] ?>" <?= (isset($_GET['teacher']) && in_array($teacher['id'], $_GET['teacher'])) ? 'checked' : '' ?> /> <?= $teacher['name'] ?></label>
<?php endforeach; ?>
</div>
<div class="filter-group">
<h4>Danh mục</h4>
<?php foreach ($categoriesList as $categories): ?>
<label><input type="checkbox" name="category[]" value="<?= $categories['id'] ?>" <?= (isset($_GET['category']) && in_array($categories['id'], $_GET['category'])) ? 'checked' : '' ?> /> <?= $categories['name'] ?></label>
<?php endforeach; ?>
</div>
<div class="filter-group">
<h4>Sắp xếp theo</h4>
<select name="sort">
<option value="price_asc" <?= (isset($_GET['sort']) && $_GET['sort'] == 'price_asc') ? 'selected' : '' ?>>Giá tăng dần</option>
<option value="price_desc" <?= (isset($_GET['sort']) && $_GET['sort'] == 'price_desc') ? 'selected' : '' ?>>Giá giảm dần</option>
<option value="newest" <?= (isset($_GET['sort']) && $_GET['sort'] == 'newest') ? 'selected' : '' ?>>Mới nhất</option>
<option value="oldest" <?= (isset($_GET['sort']) && $_GET['sort'] == 'oldest') ? 'selected' : '' ?>>Cũ nhất</option>
</select>
</div>
<div style="display: flex; gap: 10px;">
<button type="submit" class="btn" style="flex: 1;">Áp dụng bộ lọc</button>
<a href="?ctrl=course&act=list" class="btn" style="flex: 1; text-align: center; text-decoration: none; display: inline-block; background-color: #6c757d;">Xóa bộ lọc</a>
</div>
</form>
<div class="course-list">
<!-- Một dòng 4 box khóa học -->
<div class="courses">
<?php if (empty($coursesList)): ?>
<div style="text-align: center; padding: 50px; color: #666;">
<h3>Không tìm thấy khóa học nào</h3>
<p>Vui lòng thử lại với bộ lọc khác</p>
</div>
<?php else: ?>
<?php foreach ($coursesList as $course) : ?>
<div class="course">
<img src="public/img/courses/<?= $course['image'] ?>" alt="<?= $course['title'] ?>" />
<h3><?= $course['title'] ?></h3>
<p><?= number_format($course['price']) ?></p>
<a href="?ctrl=course&act=detail&id=<?= $course['id'] ?>" class="btn">Đăng ký</a>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</main>
File này chia làm hai phần rõ rệt: <form class="sidebar"> bên trái và <div class="course-list"> bên phải. CSS xếp chúng cạnh nhau.
Một chi tiết đặt tên đáng chú ý: ở vòng lặp danh mục, tác giả viết foreach ($categoriesList as $categories) — biến tạm đặt tên số nhiều $categories, trái với quy ước ở mục 2.2. Đúng ra phải là $category.
Không gây lỗi, nhưng gây khó đọc: $categories['name'] trông như đang lấy name của cả mảng danh mục. Hãy gõ y hệt để khớp repo, và đừng bắt chước thói quen này.
Nút "Xóa bộ lọc" không phải nút bấm mà là một thẻ <a> trỏ về ?ctrl=course&act=list không kèm tham số nào. Đơn giản và hiệu quả: không tham số nghĩa là không lọc.
⚠️ Lưu ý: Ô nhập từ khóa có dùng
htmlspecialchars($_GET['keyword']), nhưng<?= $teacher['name'] ?>,<?= $categories['name'] ?>và<?= $course['title'] ?>thì không.Đây đúng là kiểu thiếu nhất quán mà bạn đã gặp ở Bài 10: chỗ thì lọc, chỗ thì không, trong cùng một file. Nguyên nhân thường là tác giả copy đoạn có
htmlspecialcharstừ nơi khác về mà không rà soát toàn bộ.Đây là lỗi số 10 trong Bài 22.
4. Chạy thử
Mở http://localhost/EZCode_khoahoc/?ctrl=course&act=list, hoặc bấm mục Khóa học trên menu.
Kết quả đúng:
- Cột trái — khung lọc có tiêu đề "Lọc khóa học", ô tìm theo tên, danh sách 12 giảng viên có ô tick, danh sách 33 danh mục có ô tick, ô chọn sắp xếp, và hai nút.
- Cột phải — lưới 14 khóa học giống trang chủ.
- Bấm "Áp dụng bộ lọc" chưa có tác dụng gì — kết quả vẫn là 14 khóa học. Đúng như dự kiến, vì bạn đang dùng
getAll()tạm thời. Bài 12 sẽ làm cho nó hoạt động.
Điều đáng làm ngay: tick vài ô rồi bấm "Áp dụng bộ lọc", sau đó nhìn thanh địa chỉ:
?ctrl=course&act=list&keyword=&teacher%5B%5D=1&teacher%5B%5D=5&category%5B%5D=3&sort=newest
Bạn thấy đủ mọi thứ đã học ở mục 2.5 và 2.6: ctrl và act được giữ nhờ ô ẩn, các ô tick thành teacher[] lặp lại nhiều lần.
Và quan trọng nhất: tải lại trang, các ô bạn vừa tick vẫn còn tick. Đó là in_array() đang làm việc.
Bảng xử lý sự cố:
| Hiện tượng | Nguyên nhân | Cách sửa |
|---|---|---|
Call to undefined method Courses::filter() |
Chưa sửa tạm dòng gọi | Đổi thành getAll() theo ghi chú ở mục 3.3 |
Undefined variable $teachersList |
Controller thiếu khối Model User | Kiểm tra lại mục 3.3 |
| Nhóm lọc giảng viên trống | Bảng users không có ai role = 'teacher' |
Kiểm tra trong phpMyAdmin |
| Bấm lọc xong về trang chủ | Thiếu hai ô hidden |
Kiểm tra lại hai dòng đầu của form |
in_array(): Argument #2 must be of type array |
$_GET['teacher'] không phải mảng |
Thiếu [] trong name="teacher[]" |
5. Bài tập
Bài tập 1
Thêm vào Models/Category.php phương thức:
function getById($id)
Nó trả về một danh mục theo id, dùng queryOne().
Sau đó sửa Views/course_list.php để mỗi thẻ khóa học hiện thêm tên danh mục của nó.
Gợi ý: bạn sẽ cần tạo object
Categorytrong View, hoặc tốt hơn là dựng sẵn một mảng tra cứu trong Controller. Hãy thử cách thứ hai và giải thích vì sao nó tốt hơn.
Bài tập 2
Thêm vào đầu cột phải của Views/course_list.php một dòng hiển thị số kết quả tìm được:
Tìm thấy 14 khóa học
Dòng này chỉ hiện khi có kết quả, không hiện khi danh sách rỗng.
Bài tập 3
Thêm vào Models/User.php phương thức countTeachers() trả về số giảng viên, rồi hiển thị nó ở tiêu đề nhóm lọc:
Tên giảng viên (12)
6. Đáp án
Đáp án bài tập 1
📄 Models/Category.php — thêm vào trước dấu } cuối
function getById($id)
{
$sql = "SELECT * FROM categories WHERE id=?";
return $this->db->queryOne($sql, $id);
}
Cách làm kém — gọi Model từ trong View:
<?php foreach ($coursesList as $course) : ?>
<?php
$categoryModel = new Category();
$cat = $categoryModel->getById($course['category_id']);
?>
<p>Danh mục: <?= $cat['name'] ?></p>
<?php endforeach; ?>
Cách này chạy được nhưng sai về nguyên tắc và tệ về hiệu năng:
- View gọi Model — phá vỡ phân tầng MVC ở Bài 9 mục 2.1. View chỉ được hiển thị.
- Mỗi vòng lặp một truy vấn — 14 khóa học là 14 câu SQL. Đây gọi là vấn đề "N+1 truy vấn", và nó giết hiệu năng khi danh sách dài.
- Mỗi vòng lặp một
new Category()— tức là 14 kết nối database mới, đúng lỗi số 16 mà Bài 6 đã cảnh báo.
Cách làm tốt — dựng sẵn mảng tra cứu trong Controller:
📄 Controllers/CourseController.php — thêm sau khối Category
// Dựng mảng tra cứu: id danh mục -> tên danh mục
$categoryNames = [];
foreach ($categoriesList as $cat) {
$categoryNames[$cat['id']] = $cat['name'];
}
📄 Views/course_list.php — trong vòng lặp khóa học
<p>Danh mục: <?= $categoryNames[$course['category_id']] ?? 'Không rõ' ?></p>
Vì sao tốt hơn:
- Chỉ một truy vấn lấy 33 danh mục — mà truy vấn đó vốn đã có sẵn để dựng thanh lọc.
- Việc chuẩn bị dữ liệu nằm ở Controller, đúng chỗ.
- Tra cứu trong mảng PHP nhanh hơn hỏi database hàng nghìn lần.
?? 'Không rõ'phòng trường hợp khóa học trỏ tới danh mục đã bị xóa.
Cách làm tốt nhất thì lại là dùng INNER JOIN để MySQL trả về luôn tên danh mục trong cùng một truy vấn. Bài 13 sẽ dạy JOIN, và bạn sẽ thấy Courses::getById() làm đúng điều đó với tên giảng viên.
Đáp án bài tập 2
📄 Views/course_list.php — sửa nhánh else
<?php else: ?>
<p style="width: 100%; color: #666;">Tìm thấy <?= count($coursesList) ?> khóa học</p>
<?php foreach ($coursesList as $course) : ?>
Đặt trong nhánh else nên nó tự động không hiện khi danh sách rỗng — nhánh if đã lo trường hợp đó với thông báo "Không tìm thấy khóa học nào".
Thuộc tính width: 100% cần thiết vì thẻ cha .courses dùng bố cục lưới; không có nó thì dòng chữ sẽ bị ép vào một ô lưới hẹp.
Đáp án bài tập 3
📄 Models/User.php — thêm vào trước dấu } cuối
function countTeachers()
{
$sql = "SELECT COUNT(*) as total FROM users WHERE role=?";
$result = $this->db->queryOne($sql, 'teacher');
return $result['total'] ?? 0;
}
📄 Controllers/CourseController.php — thêm sau $teachersList
$teacherCount = $userModel->countTeachers();
📄 Views/course_list.php
<h4>Tên giảng viên (<?= $teacherCount ?>)</h4>
Ba điều đáng học từ phương thức này:
Thứ nhất, COUNT(*) luôn trả về một dòng nên dùng queryOne() chứ không phải query(). Kết quả là mảng kết hợp có đúng một khóa.
Thứ hai, as total đặt tên cho cột kết quả. Không có nó, tên cột sẽ là chuỗi COUNT(*) — khó viết trong PHP vì có dấu ngoặc. Với as total, bạn viết gọn $result['total'].
Thứ ba, ?? 0 phòng trường hợp null. Với COUNT(*) thì không bao giờ null (không có dòng nào thì trả về 0), nhưng với SUM() thì có — không có dòng nào thì SUM trả về null chứ không phải 0.
Đây chính xác là khuôn mẫu mà Models/Courses.php dùng cho năm phương thức thống kê ở Bài 20:
function countByTeacher($teacher_id)
{
$sql = "SELECT COUNT(*) as total FROM courses WHERE teacher_id=?";
$result = $this->db->queryOne($sql, $teacher_id);
return $result['total'] ?? 0;
}
Giống hệt. Bạn vừa viết trước một phương thức của Bài 20 mà không biết.
Cách thay thế bằng PHP thuần:
$teacherCount = count($teachersList);
Cũng cho ra 12, và không tốn thêm truy vấn nào vì $teachersList đã có sẵn.
Vậy cách nào tốt hơn? Trong trường hợp này, count() tốt hơn — dữ liệu đã nằm trong tay rồi, hỏi lại database là lãng phí.
Nhưng nếu bạn chỉ cần con số mà không cần danh sách, thì COUNT(*) tốt hơn nhiều: MySQL chỉ trả về một số thay vì 12 dòng dữ liệu đầy đủ.
Nguyên tắc: đã có mảng thì dùng count(); chỉ cần con số thì hỏi COUNT(*).
➡️ Bài tiếp theo: Bài 12 — Bộ lọc và tìm kiếm
⬅️ Về mục lục · Bài trước
All rights reserved