EzCode # Bài 10 — Layout và trang chủ chạy được
Học xong bài này bạn sẽ:
- Tách header và footer thành layout dùng chung cho cả website
- Viết Model đầu tiên lấy dữ liệu thật từ bảng
courses- Mở trình duyệt và thấy trang chủ EzCode chạy với 14 khóa học từ database
1. Vấn đề đặt ra
EzCode có 22 trang. Cả 22 đều dùng chung phần đầu trang: thẻ <head>, nạp CSS, logo, ô tìm kiếm, thanh menu. Và cả 22 đều dùng chung phần cuối: footer, nạp JavaScript, đóng thẻ </body></html>.
Nếu mỗi View tự viết đủ hai phần đó, bạn sẽ có 22 bản sao của cùng một đoạn HTML dài 50 dòng. Thêm một mục vào menu là sửa 22 file.
Bạn đã có sẵn công cụ giải quyết từ Bài 6: include. Việc còn lại là cắt phần chung ra hai file riêng và cho index.php nạp chúng bao quanh nội dung.
Bài này là bài quan trọng nhất của Phần II, vì kết thúc nó bạn sẽ có một website chạy thật với dữ liệu thật — không còn là bài tập trong thư mục hoc/ nữa.
2. Kiến thức mới
2.1. Layout — cắt phần chung ra
Ý tưởng đơn giản: cắt một trang HTML hoàn chỉnh thành ba mảnh.
┌─────────────────────────────┐
│ layout_header.php │ <!DOCTYPE html> ... <header>...</header>
├─────────────────────────────┤
│ nội dung riêng của trang │ <main> ... </main>
├─────────────────────────────┤
│ layout_footer.php │ <footer>...</footer> ... </body></html>
└─────────────────────────────┘
Điểm đáng chú ý: layout_header.php không phải là một file HTML hợp lệ. Nó mở thẻ <html> và <body> nhưng không đóng. layout_footer.php thì ngược lại — nó đóng những thẻ mà nó chưa từng mở.
Chỉ khi ghép ba mảnh lại theo đúng thứ tự, bạn mới có HTML hoàn chỉnh. Đây là điều bình thường với layout, nhưng lần đầu nhìn sẽ thấy lạ.
2.2. Ai nạp layout — index.php hay View?
index.php của EzCode nạp cả hai:
include_once "./Views/layout_header.php"; // dòng 7
// ... controller chạy, in nội dung riêng ...
include_once "./Views/layout_footer.php"; // dòng 31
Nghĩa là mọi View chỉ cần viết phần giữa. Không View nào phải lo tới <head> hay <footer>.
Đây là lợi ích trực tiếp của front controller mà bạn dựng ở Bài 9: vì mọi request đều qua một cửa, đặt layout ở cửa đó là đủ cho tất cả.
Có một View phá vỡ quy tắc này.
Views/payment_history.phptựinclude_oncelại cả header lẫn footer, trong khiindex.phpđã nạp rồi. Kết quả là trang lịch sử thanh toán có hai header và hai footer chồng lên nhau.Bạn sẽ thấy tận mắt ở Bài 21. Đây là lỗi số 6 trong Bài 22.
2.3. include_once chạy trong cùng phạm vi biến
Bài 9 mục 3.2 đã nói qua, nhưng đây là điểm mấu chốt nên nhắc lại rõ.
include không phải gọi hàm. Nó chèn nội dung file vào đúng chỗ và chạy trong cùng phạm vi biến với nơi gọi.
Nghĩa là:
public function home()
{
$coursesList = $coursesModel->getAll(); // tạo biến ở đây
include_once "./Views/page_home.php"; // View thấy $coursesList
}
PHP xử lý như thể bạn chép toàn bộ nội dung page_home.php dán vào chỗ dòng include. Vì $coursesList đã khai báo phía trên trong cùng hàm, View dùng được ngay.
Hệ quả cần nhớ: biến trong View và biến trong Controller là cùng một biến. Nếu View sửa nó, Controller cũng thấy. Đây là cách truyền dữ liệu đơn giản nhất nhưng cũng lỏng lẻo nhất.
2.4. Đường dẫn tương đối tính theo file chạy chính
Đây là chỗ người mới hay sai.
Trong Controllers/PageController.php có dòng:
include_once "./Models/Courses.php";
Dấu ./ nghĩa là "thư mục hiện tại". Nhưng hiện tại ở đâu?
Không phải thư mục Controllers/. Mà là thư mục của file chạy chính, tức index.php ở gốc project.
Lý do: PageController.php được include từ index.php, và PHP tính đường dẫn tương đối theo file khởi đầu chứ không theo file đang chèn.
Quy tắc cho EzCode: mọi đường dẫn include trong Model, Controller và View đều viết từ gốc project:
include_once "./Models/Database.php";
include_once "./Controllers/PageController.php";
include_once "./Views/page_home.php";
Không bao giờ viết include_once "../Models/Database.php"; bên trong file Controller — nó sẽ trỏ ra ngoài project.
Ngoại lệ duy nhất là các file bài tập trong thư mục
hoc/mà bạn tự chạy trực tiếp. Ở Bài 8 bạn viếtinclude_once "../Models/Database.php";và nó đúng, vì lúc đóhoc/bai08.phpchính là file chạy chính.
2.5. Đường dẫn tài nguyên trong HTML
Khác với include của PHP, đường dẫn trong thẻ <link>, <img>, <script> được trình duyệt hiểu, không phải PHP.
<link rel="stylesheet" href="public/css/style.css" />
Trình duyệt ghép nó với địa chỉ hiện tại. Vì mọi trang EzCode đều có địa chỉ dạng http://localhost/EZCode_khoahoc/?ctrl=..., thư mục gốc luôn là EZCode_khoahoc/, nên public/css/style.css luôn trỏ đúng.
Đây là một lợi ích nữa của front controller: địa chỉ không bao giờ đi sâu vào thư mục con, nên đường dẫn tương đối luôn ổn định.
2.6. Thư mục templateEzCode/
Trong project có thư mục templateEzCode/ chứa 15 file HTML tĩnh: index.html, course_list.html, user_login.html...
Đây là bản thiết kế gốc — giao diện được dựng bằng HTML thuần trước, dữ liệu viết cứng, chưa có PHP.
Quy trình chuyển từ HTML tĩnh sang View PHP gồm ba bước:
- Cắt phần chung — mọi file HTML đều có chung phần đầu và cuối, tách chúng ra
layout_header.phpvàlayout_footer.php. - Thay dữ liệu cứng bằng biến —
<h3>Khóa học HTML CSS</h3>thành<h3><?= $course['title'] ?></h3>. - Thay khối lặp bằng vòng lặp — 14 thẻ
<div class="course">viết tay thành một thẻ nằm trongforeach.
Bạn có thể tự đối chiếu: mở templateEzCode/index.html và Views/page_home.php cạnh nhau. Chúng là cùng một giao diện, một bên viết cứng và một bên động.
Đây cũng là quy trình bạn nên dùng cho project của riêng mình sau này: dựng giao diện tĩnh cho xong, rồi mới "thổi hồn" PHP vào.
2.7. Ba đoạn JavaScript trong script.js
File public/js/script.js chỉ có 50 dòng và chứa đúng ba tính năng, mỗi tính năng bọc trong một khối try { ... } catch (error) {}:
- Slider — tự động đổi ảnh banner mỗi 3 giây. Dùng ở trang chủ.
- Accordion — bấm vào tên bài học thì mở nội dung ra. Dùng ở trang chi tiết khóa học, Bài 13.
- Menu toggle — nút ba gạch để mở menu trên điện thoại. Dùng ở mọi trang.
Vì sao mỗi đoạn bọc trong try/catch rỗng?
Vì file này được nạp ở mọi trang, nhưng không trang nào có đủ cả ba thành phần. Trang chủ không có accordion, trang đăng nhập không có slider.
Nếu không bọc try/catch, đoạn accordion sẽ báo lỗi trên trang chủ, và JavaScript dừng hẳn — nghĩa là đoạn menu toggle phía dưới không chạy nữa.
catch (error) {} với thân rỗng nghĩa là "gặp lỗi thì im lặng bỏ qua, chạy tiếp đoạn sau".
Đây là cách làm thô nhưng hiệu quả. Cách tốt hơn là kiểm tra phần tử có tồn tại không trước khi xử lý:
const slider = document.querySelector(".slider");
if (slider) {
// chỉ chạy khi có slider
}
Nhược điểm của try/catch rỗng là nó nuốt luôn cả những lỗi thật mà bạn cần biết. Bạn cứ gõ theo repo, và ghi nhớ điều này cho project riêng.
3. Áp dụng vào EzCode
Bài này bạn tạo 5 file mới và sửa 1 file. Làm theo đúng thứ tự dưới đây.
3.1. Chép CSS và JavaScript vào
📁
public/css/style.css— tự tạo (không so khớp repo)
Chép nguyên file style.css từ repo vào thư mục public/css/ của bạn.
File này dài 1448 dòng và giáo trình không dạy CSS chi tiết — đó là chủ đề riêng. Bạn chỉ cần chép nguyên vào.
Vài class chính để bạn biết chúng làm gì khi đọc View:
| Class | Tác dụng |
|---|---|
.courses |
Lưới hiển thị các thẻ khóa học |
.course |
Một thẻ khóa học |
.btn |
Nút bấm màu xanh |
.banner .slider |
Khung ảnh quảng cáo ở trang chủ |
.dashboard-page .dashboard-content |
Bố cục hai cột của khu vực giảng viên |
.profile-page .profile-sidebar |
Bố cục hai cột của trang cá nhân |
Tạo tiếp thư mục public/img/ và chép toàn bộ file ảnh từ repo vào, gồm cả thư mục con public/img/courses/.
Bây giờ tới JavaScript. Tạo file public/js/script.js và chép nguyên từ repo. Ta xem đoạn đầu của nó:
📁
public/js/script.js— đọc trước
try {
// Simple slider
let images = document.querySelectorAll(".slider img");
let current = 0;
function showSlide(index) {
images.forEach((img) => img.classList.remove("active"));
images[index]?.classList.add("active");
}
function nextSlide() {
current = (current + 1) % images.length;
showSlide(current);
}
setInterval(nextSlide, 3000);
showSlide(current);
} catch (error) {}
Đây là đoạn slider. Cách nó hoạt động:
querySelectorAll(".slider img")lấy mọi thẻ<img>bên trong phần tử có classslider. Ở trang chủ có 3 ảnh.- CSS quy định
.banner img { display: none; }và.banner img.active { display: block; }— nghĩa là chỉ ảnh có classactivemới hiện. showSlide(index)gỡ classactivekhỏi mọi ảnh rồi gắn vào ảnh thứindex.nextSlide()tăng chỉ số lên 1. Phép chia lấy dư% images.lengthkhiến nó quay vòng: 0, 1, 2, rồi lại 0.setInterval(nextSlide, 3000)gọinextSlidemỗi 3000 mili giây, tức 3 giây.
Ký hiệu ?. trong images[index]?.classList gọi là toán tử truy cập an toàn: nếu images[index] không tồn tại thì bỏ qua thay vì báo lỗi.
3.2. Tạo layout header
Đây là file HTML dài nhất mà bạn gõ ở bài này.
📁
Views/layout_header.php— tạo mới
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EzCode - Khóa học trực tuyến</title>
<link rel="stylesheet" href="public/css/style.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" />
</head>
<body>
<!-- Header -->
<header>
<div class="logo">
<a href="?ctrl=page&act=home"
><img src="public/img/Logo.png" alt="Logo" style="width: 140px"
/></a>
</div>
<button id="menu-toggle" class="menu-toggle">☰</button>
<input type="search" placeholder="Tìm khóa học..." />
<nav id="main-menu">
<ul>
<li><a href="?ctrl=page&act=home">Trang chủ</a></li>
<li><a href="?ctrl=course&act=list">Khóa học</a></li>
<?php if (isset($_SESSION['user_id'])): ?>
<!-- User đã đăng nhập -->
<li><a href="?ctrl=user&act=profile">Xin chào, <?= htmlspecialchars($_SESSION['user_name']) ?></a></li>
<?php if ($_SESSION['user_role'] === 'teacher'): ?>
<li><a href="?ctrl=teacher&act=dashboard">Dashboard</a></li>
<?php endif; ?>
<li><a href="?ctrl=user&act=logout">Đăng xuất</a></li>
<?php else: ?>
<!-- User chưa đăng nhập -->
<li><a href="?ctrl=user&act=register">Đăng ký</a></li>
<li><a href="?ctrl=user&act=login">Đăng nhập</a></li>
<?php endif; ?>
</ul>
</nav>
</header>
Vài điểm đáng chú ý.
Toàn file thụt vào 2 khoảng trắng. Kể cả dòng <!DOCTYPE html>. Đây là dấu vết của việc cắt từ file HTML gốc ra. Trông lạ nhưng không ảnh hưởng gì — HTML bỏ qua khoảng trắng thừa. Hãy gõ y hệt.
File không đóng thẻ <body> và <html>. Đúng như mục 2.1 đã nói, chúng nằm trong footer.
☰ là mã HTML của ký tự ba gạch ngang ☰, dùng làm nút menu trên điện thoại.
Ô <input type="search"> không nằm trong <form> nào, nghĩa là gõ vào rồi bấm Enter sẽ không có gì xảy ra. Đây là một chức năng chưa hoàn thiện của project — giao diện có nhưng chưa nối vào đâu.
Khối <?php if (isset($_SESSION['user_id'])): ?> dùng đúng cú pháp thay thế bạn học ở Bài 2 mục 2.6, và kiểm tra session bạn học ở Bài 5.
Bây giờ nó luôn rơi vào nhánh else vì bạn chưa làm chức năng đăng nhập. Menu sẽ hiện "Đăng ký" và "Đăng nhập". Bài 15 sẽ làm cho nhánh trên hoạt động.
Chú ý dòng 27 dùng htmlspecialchars($_SESSION['user_name']) — có lọc HTML. Đây là một trong số ít chỗ EzCode làm đúng. Hãy nhớ điều này để so sánh với các View khác.
3.3. Tạo layout footer
📁
Views/layout_footer.php— tạo mới
<footer>
<div class="footer-left">
<a href="index.html"
><img src="public/img/Logo.png" alt="Logo" style="width: 140px"
/></a>
</div>
<div class="footer-right">© 2025 EzCode. Bản quyền đã được bảo hộ.</div>
</footer>
<script src="public/js/script.js"></script>
</body>
</html>
Thẻ <script> đặt ở cuối trang, ngay trước </body>. Đây là thực hành chuẩn: trình duyệt vẽ HTML từ trên xuống, gặp thẻ <script> là dừng lại tải và chạy nó. Đặt ở đầu thì người dùng nhìn trang trắng lâu hơn; đặt ở cuối thì nội dung hiện ra trước, JavaScript chạy sau.
Ngoài ra JavaScript trong file này cần các phần tử HTML đã tồn tại mới thao tác được. Đặt ở đầu thì querySelectorAll(".slider img") sẽ không tìm thấy gì.
Một lỗi nhỏ trong file này: logo footer trỏ tới
href="index.html"— một file không tồn tại trong project. Đúng ra phải là?ctrl=page&act=homenhư logo header. Đây là dấu vết còn sót từ bản HTML tĩnh trongtemplateEzCode/.Lỗi này không nằm trong danh sách 17 lỗi chính của Bài 22 vì nó rất nhỏ, nhưng bạn có thể tự sửa. Nó cũng là một gợi ý tốt cho bài tập 3 của Bài 22.
3.4. Tạo Model đầu tiên
📁
Models/Courses.php— tạo mới
<?php
include_once "./Models/Database.php";
class Courses
{
private $db;
function __construct()
{
$this->db = new Database();
}
function getAll()
{
$sql = "SELECT * FROM courses WHERE status = 1 ORDER BY id DESC";
return $this->db->query($sql);
}
Chú ý: khối trên chưa đóng class. Hãy thêm một dòng
}ở cuối file để đóng lại.Từ Bài 12 trở đi, mỗi khi thêm phương thức mới bạn sẽ chèn nó vào trước dấu
}cuối cùng này. ModelCoursessẽ lớn dần qua 5 bài nữa và cuối cùng có 13 phương thức.
So sánh với Models/Category.php mà bạn đã đọc ở Bài 6 — cấu trúc giống hệt: include_once, private $db, __construct tạo kết nối, rồi các phương thức truy vấn. Đây là khuôn mẫu chung của cả 6 Model.
Câu SQL đáng phân tích:
SELECT * FROM courses WHERE status = 1 ORDER BY id DESC
SELECT *— lấy mọi cột. Kết quả là mảng kết hợp có 8 khóa, đúng 8 tên cột của bảngcoursesnhư Bài 7 đã học.WHERE status = 1— chỉ lấy khóa học đang hiện. Khóa học bị giảng viên ẩn (status = 0) sẽ không xuất hiện.ORDER BY id DESC— sắp xếp id giảm dần, nghĩa là khóa học mới nhất lên đầu.
Không có tham số ? nào vì câu này không nhận dữ liệu từ người dùng. Đây là trường hợp hiếm hoi an toàn khi không dùng placeholder.
3.5. Trả PageController về nguyên bản
Ở Bài 9 bạn đã sửa tạm phương thức home() thành một dòng echo. Bây giờ trả lại đầy đủ.
📁
Controllers/PageController.php— sửa
<?php
class PageController
{
public function home()
{
// Logic for displaying the home page
// echo "Welcome to the home page!";
include_once "./Models/Courses.php"; // Include the Courses model
$coursesModel = new Courses(); // Create an instance of the Courses model
$coursesList = $coursesModel->getAll(); // Fetch all courses
include_once "./Views/page_home.php";
}
}
Bốn bước quen thuộc: nạp Model, tạo object, gọi phương thức lấy dữ liệu, nạp View.
Biến $coursesList được tạo ở dòng 11 và sẽ được Views/page_home.php dùng ở dòng 14 — nhờ cơ chế cùng phạm vi biến ở mục 2.3.
Hai dòng ghi chú // Logic for displaying the home page và // echo "Welcome to the home page!"; là dấu vết của tác giả khi mới dựng khung. Giữ nguyên.
Hai dòng trống liền nhau trước include_once "./Views/page_home.php"; cũng vậy — không có ý nghĩa gì, nhưng bạn cứ gõ y hệt để script kiểm tra khớp.
3.6. Tạo View trang chủ
📁
Views/page_home.php— tạo mới
<!-- Banner -->
<section class="banner">
<div class="slider">
<img src="public/img/banner.png" alt="Khóa học EzCode" />
<img src="public/img/banner.png" alt="Khóa học EzCode" />
<img src="public/img/banner.png" alt="Khóa học EzCode" />
</div>
</section>
<main>
<!-- Khóa học -->
<h2>Khóa học nổi bật</h2>
<div class="courses">
<?php foreach ($coursesList as $course) : ?>
<div class="course">
<img src="public/img/courses/<?= $course['image']?>" alt="<?= $course['title']?>" />
<h3><?= $course['title']?></h3>
<p>Giá: <?= number_format($course['price'])?></p>
<a href="?ctrl=course&act=detail&id=<?=$course['id']?>" class="btn">Đăng ký</a>
</div>
<?php endforeach; ?>
</div>
</main>
Bạn đã phân tích kỹ vòng lặp này ở Bài 3 mục 3. Bây giờ nó chạy với dữ liệu thật.
Ba thẻ <img> trong slider đều trỏ tới cùng một file banner.png. Nghĩa là ảnh có đổi cũng nhìn như không đổi. Đây là chỗ giữ sẵn để sau này thay bằng ba ảnh khác nhau.
File không có <head>, không có <body>. Nó chỉ là phần giữa, đúng như mục 2.1.
⚠️ Lưu ý: Đường dẫn ảnh khóa học là
public/img/courses/, nhưng ảnh của 12 khóa học mẫu lại nằm ởpublic/img/. Chỉ 2 khóa học (id 20 và 21) có ảnh đúng chỗ vì chúng được tải lên qua chức năng của Bài 18.Kết quả là khi bạn mở trang chủ, 12 trong 14 khóa học sẽ bị vỡ ảnh. Đây là hành vi đúng của code repo, không phải bạn gõ sai. Đó là lỗi số 17 trong Bài 22.
⚠️ Lưu ý:
<?= $course['title']?>in dữ liệu từ database ra HTML mà không quahtmlspecialchars(). So sánh vớilayout_header.phpở mục 3.2 — chỗ đó có lọc. Sự thiếu nhất quán này là lỗi số 10 trong Bài 22.
4. Chạy thử
Kiểm tra cấu trúc thư mục của bạn phải như sau:
C:\xampp\htdocs\EZCode_khoahoc\
├── index.php
├── Controllers\
│ └── PageController.php
├── Models\
│ ├── Database.php
│ └── Courses.php
├── Views\
│ ├── layout_header.php
│ ├── layout_footer.php
│ └── page_home.php
└── public\
├── css\style.css
├── js\script.js
└── img\ (có Logo.png, banner.png và các ảnh khóa học)
Mở http://localhost/EZCode_khoahoc/.
Kết quả đúng:
- Header xanh với logo EzCode bên trái, ô tìm kiếm ở giữa, menu bên phải gồm: Trang chủ, Khóa học, Đăng ký, Đăng nhập.
- Banner chiếm hết chiều ngang.
- Tiêu đề "Khóa học nổi bật".
- 14 thẻ khóa học xếp thành lưới. Thẻ đầu tiên là "Internet of Things" giá 25.000 vì
ORDER BY id DESC. - Hầu hết ảnh bị vỡ — đúng như hộp cảnh báo.
- Footer xanh ở cuối trang.
Đây là khoảnh khắc quan trọng. Bạn vừa nối xong cả bốn tầng: index.php định tuyến, PageController điều phối, Models/Courses truy vấn MySQL, Views/page_home hiển thị. Từ đây trở đi, mỗi bài chỉ là thêm chức năng vào bộ khung đã chạy.
Kiểm chứng thêm: bấm Ctrl + U xem mã nguồn. Bạn thấy HTML hoàn chỉnh có đủ <head>, 14 thẻ <div class="course">, và </html> ở cuối — ba mảnh layout đã ghép lại đúng.
Bảng xử lý sự cố:
| Hiện tượng | Nguyên nhân | Cách sửa |
|---|---|---|
| Trang trắng hoàn toàn | Lỗi PHP bị giấu | Kiểm tra error_reporting ở đầu index.php |
Failed opening './Models/Courses.php' |
Sai tên file hoặc sai thư mục | Tên file là Courses.php có chữ s |
Undefined variable $coursesList |
Controller chưa gán biến | Kiểm tra lại mục 3.5 |
| Trang không có màu, chữ đen trên nền trắng | CSS chưa nạp được | Kiểm tra public/css/style.css tồn tại |
Connection failed: Unknown database |
Chưa import database | Làm lại Bài 7 |
| Hiện đủ khung nhưng không có khóa học nào | Bảng courses rỗng, hoặc mọi dòng có status = 0 |
Kiểm tra trong phpMyAdmin |
| Chữ tiếng Việt thành ký tự lạ | Thiếu <meta charset="UTF-8"> |
Kiểm tra lại layout_header.php |
5. Bài tập
Bài tập 1
Thêm vào Views/page_home.php một dòng ngay dưới tiêu đề "Khóa học nổi bật", hiển thị tổng số khóa học đang có, dạng:
Hiện có 14 khóa học
Con số phải lấy động bằng count(), không viết cứng.
Bài tập 2
Thêm vào Models/Courses.php một phương thức mới:
function getNewest($limit)
Phương thức này trả về $limit khóa học mới nhất. Dùng dấu ? cho tham số LIMIT.
Sau đó sửa PageController::home() để trang chủ chỉ hiện 4 khóa học thay vì 14.
Gợi ý:
LIMIT ?hoạt động với prepared statement, nhưng một số cấu hình MySQL sẽ coi tham số là chuỗi và báo lỗi cú pháp. Nếu gặp, hãy ép kiểu bằng(int)trước khi truyền vào.
Bài tập 3
Thêm vào Views/page_home.php khối xử lý trường hợp không có khóa học nào:
<?php if (empty($coursesList)): ?>
...
<?php else: ?>
...vòng lặp hiện tại...
<?php endif; ?>
Thông báo phải là "Chưa có khóa học nào" và có liên kết về trang danh sách.
Cách thử nghiệm: tạm đổi câu SQL trong getAll() từ status = 1 thành status = 99 rồi tải lại trang. Sau khi kiểm chứng xong, nhớ đổi lại status = 1.
6. Đáp án
Đáp án bài tập 1
📄 Views/page_home.php — sửa phần <main>
<main>
<!-- Khóa học -->
<h2>Khóa học nổi bật</h2>
<p>Hiện có <?= count($coursesList) ?> khóa học</p>
<div class="courses">
Chỉ một dòng thêm vào. count() là hàm bạn đã học ở Bài 3 mục 2.5.
Đây cũng là lúc thấy rõ lợi ích của MVC: bạn thay đổi cách hiển thị mà không đụng tới Model hay Controller. Câu truy vấn không đổi, dữ liệu không đổi, chỉ cách trình bày đổi.
Đáp án bài tập 2
📄 Models/Courses.php — thêm phương thức trước dấu } cuối cùng
function getNewest($limit)
{
$sql = "SELECT * FROM courses WHERE status = 1 ORDER BY id DESC LIMIT ?";
return $this->db->query($sql, (int)$limit);
}
📄 Controllers/PageController.php — sửa một dòng
$coursesList = $coursesModel->getNewest(4); // Fetch 4 newest courses
Kết quả: trang chủ chỉ còn 4 thẻ khóa học, gồm id 21, 20, 12, 11.
Về (int)$limit: ép kiểu là cần thiết vì mọi giá trị từ $_GET đều là chuỗi (Bài 4 mục 2.3), và một số phiên bản MySQL sẽ dịch LIMIT '4' có nháy thành lỗi cú pháp. Ở đây ta truyền số trực tiếp nên không bắt buộc, nhưng viết sẵn để phòng khi sau này $limit đến từ URL.
Đây là bài tập quan trọng vì nó cho bạn thấy quy trình thêm chức năng vào EzCode: thêm phương thức vào Model, rồi gọi nó từ Controller. Bạn sẽ lặp lại quy trình này hàng chục lần trong 12 bài tới.
Sau khi kiểm chứng xong, hãy đổi lại thành getAll() để trang chủ khớp với repo.
Đáp án bài tập 3
📄 Views/page_home.php
<main>
<!-- Khóa học -->
<h2>Khóa học nổi bật</h2>
<div class="courses">
<?php if (empty($coursesList)): ?>
<div style="text-align: center; padding: 50px; color: #666;">
<h3>Chưa có khóa học nào</h3>
<p>Hãy <a href="?ctrl=course&act=list" style="color: #0868b4;">xem tất cả khóa học</a></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>Giá: <?= 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>
</main>
Với status = 99, trang hiện thông báo thay vì lưới trống trơn.
Vì sao điều này quan trọng? Vòng foreach trên mảng rỗng không báo lỗi — nó chỉ đơn giản không chạy lần nào. Người dùng thấy một trang có tiêu đề "Khóa học nổi bật" rồi khoảng trắng mênh mông, và không hiểu là website hỏng hay thật sự chưa có khóa học nào.
Mẫu if (empty(...)) này được EzCode dùng ở 7 file View: course_list.php, user_course.php, payment_history.php, teacher_course.php, teacher_lessons.php, teacher_students.php, teacher_dashboard.php. Riêng page_home.php thì tác giả quên.
Bạn có thể tự đối chiếu bằng cách mở Views/course_list.php — nó sẽ được viết ở Bài 11 và có sẵn khối này. Cùng một cặp màu #666 và cùng kiểu thụt lề padding: 50px.
Có nên giữ bản sửa này không? Có. Đây là cải tiến thuần túy, không làm lệch cấu trúc code so với repo. Nhưng nếu bạn muốn khớp tuyệt đối với repo để dễ đối chiếu ở các bài sau, hãy khôi phục bản gốc.
🎉 Bạn vừa hoàn thành Phần II. Website đã chạy. Từ Bài 11 trở đi, mỗi bài thêm một chức năng vào bộ khung này.
➡️ Bài tiếp theo: Bài 11 — Danh sách khóa học
⬅️ Về mục lục · Bài trước
All rights reserved