0

PHP và MySQL Bài 29: Hoàn thiện giao diện Client, tối ưu hiệu năng (N+1 Query) và Khép lại dự án Mini CMS

Ở bài trước, chúng ta đã xây dựng thành công trang Quản trị (Admin) để đăng bài [cite: x]. Bây giờ, chúng ta sẽ làm nốt phần việc của trang Khách (Client) – nơi người dùng bình thường truy cập vào để đọc tin tức [cite: x].

Đồng thời, chúng ta sẽ mổ xẻ một lỗi thiết kế vô hình nhưng có khả năng đánh sập toàn bộ máy chủ nếu lượng truy cập tăng cao [cite: x].


1. Xây dựng Giao diện Trang chủ (Client Home)

Nhiệm vụ của trang chủ là hiển thị danh sách các bài viết mới nhất ra màn hình [cite: x]. Vì ở Bài 28, lớp Post Model đã có sẵn hàm getAllPosts(), chúng ta chỉ cần tạo một Controller và một View để xử lý việc này [cite: x].

Tại file controllers/HomeController.php (Tạo mới):

<?php
require_once 'models/Post.php';

class HomeController {
    private $postModel;

    public function __construct($pdo) {
        $this->postModel = new Post($pdo);
    }

    public function index() {
        // Lấy toàn bộ bài viết từ Database
        $posts = $this->postModel->getAllPosts();
        
        // Gọi View hiển thị
        require 'views/client/home.php';
    }
}

Tại file views/client/home.php (Tạo mới):

Quy tắc an toàn: Nhớ lại Bài 13, trước khi in bất kỳ văn bản nào từ Database ra màn hình HTML, chúng ta bắt buộc phải bọc nó qua hàm htmlspecialchars() để chống lỗ hổng mã độc XSS [cite: x].

<!DOCTYPE html>
<html>
<head>
    <title>Trang chủ - Mini CMS</title>
    <style>
        .post-card { border: 1px solid #ddd; margin-bottom: 20px; padding: 15px; }
        .meta { color: gray; font-size: 0.9em; }
    </style>
</head>
<body>
    <h1>Tin tức mới nhất</h1>

    <?php if (empty($posts)): ?>
        <p>Hiện chưa có bài viết nào.</p>
    <?php else: ?>
        <?php foreach ($posts as $post): ?>
            <div class="post-card">
                <!-- Chống XSS khi hiển thị tiêu đề -->
                <h2><?= htmlspecialchars($post['title']) ?></h2>
                
                <div class="meta">
                    Chuyên mục: <?= htmlspecialchars($post['category_name']) ?> | 
                    Tác giả: <?= htmlspecialchars($post['author_name']) ?> |
                    Ngày đăng: <?= date('d/m/Y H:i', strtotime($post['created_at'])) ?>
                </div>

                <!-- Hiển thị ảnh bìa nếu có -->
                <?php if (!empty($post['thumbnail'])): ?>
                    <img src="<?= htmlspecialchars($post['thumbnail']) ?>" alt="Thumbnail" width="200">
                <?php endif; ?>

                <p><?= htmlspecialchars(substr($post['content'], 0, 150)) ?>...</p>
                
                <a href="index.php?action=view_post&id=<?= $post['id'] ?>">Đọc tiếp</a>
            </div>
        <?php endforeach; ?>
    <?php endif; ?>
</body>
</html>

Tích hợp vào Bộ định tuyến (index.php):

Cập nhật file index.php ở thư mục gốc để Request mặc định trỏ về HomeController:

// ... code khởi tạo ...
require_once 'controllers/HomeController.php';

$homeController = new HomeController($pdo);

switch ($action) {
    case 'home':
    default:
        $homeController->index(); // Hiển thị trang chủ
        break;
    // ... các case khác ...
}

2. Bài toán sát thủ: Lỗi N+1 Query

Khi đi phỏng vấn Backend, nhà tuyển dụng rất hay hỏi: "Em hiểu thế nào về lỗi N+1 Query và cách khắc phục?" [cite: x]

Hãy tưởng tượng, nếu chúng ta viết hàm lấy bài viết theo kiểu "ngây thơ" (Lazy Loading) như sau:

  1. Câu lệnh 1: SELECT * FROM posts LIMIT 10 (Lấy ra 10 bài viết) [cite: x].
  2. Khi dùng vòng lặp foreach để in 10 bài viết này ra màn hình, cứ mỗi lần lặp đến bài nào, ta lại viết thêm 1 câu lệnh lấy tác giả: SELECT fullname FROM users WHERE id = {user_id} [cite: x].

Hậu quả:

  • Để hiển thị 10 bài viết, hệ thống phải chạy 1 câu truy vấn lấy bài viết + 10 câu truy vấn lấy tác giả = 11 câu lệnh SQL [cite: x].
  • Nếu trang chủ có 100 bài viết, hệ thống sẽ phải chịu 101 câu truy vấn [cite: x]. Khi có 1,000 người vào web cùng lúc, máy chủ Database sẽ lập tức sập vì quá tải hàng triệu kết nối [cite: x].

3. Cách khắc phục: Eager Loading với JOIN

Để khắc phục N+1, ta dùng kỹ thuật Eager Loading (Tải trước dữ liệu) [cite: x].

Nếu bạn mở lại file models/Post.php ở Bài 28, bạn sẽ thấy tôi đã cố tình sử dụng kỹ thuật này ngay từ đầu [cite: x]:

SELECT p.*, u.fullname AS author_name, c.name AS category_name 
FROM posts p
LEFT JOIN users u ON p.user_id = u.id
LEFT JOIN categories c ON p.category_id = c.id

Nhờ sử dụng LEFT JOIN, MySQL sẽ gộp tất cả thông tin bài viết, tên tác giả, tên danh mục vào chung một bảng kết quả ngay bên trong bộ nhớ của nó [cite: x]. Chỉ tốn đúng 1 câu truy vấn duy nhất để lấy ra toàn bộ dữ liệu hoàn chỉnh, dù có hiển thị 10 hay 100 bài viết đi chăng nữa [cite: x]. Hiệu năng được cải thiện hàng trăm lần [cite: x].


4. Checklist Bảo mật sinh tử trước khi đưa Web lên mạng (Deploy)

Trước khi đóng gói dự án và đẩy lên các dịch vụ Hosting/VPS thực tế, hãy rà soát 4 nguyên tắc vàng sau [cite: x]:

  1. Chống SQL Injection: Đã dùng Prepared Statements (prepareexecute) của PDO ở tất cả các hàm INSERT/UPDATE/SELECT có biến. (OK) [cite: x]
  2. Chống XSS: Đã dùng htmlspecialchars() ở mọi file View (HTML) trước khi in dữ liệu do người dùng nhập ra màn hình. (OK) [cite: x]
  3. Mã hóa mật khẩu: Đã dùng password_hash() bằng chuẩn Bcrypt. Tuyệt đối không lưu mật khẩu rõ (Plaintext) vào CSDL. (OK) [cite: x]
  4. Tắt hiển thị lỗi nghiêm trọng: Trên máy cá nhân (Localhost), bạn cần xem lỗi để fix [cite: x]. Nhưng trên máy chủ thật (Production), bạn phải tắt việc in lỗi ra màn hình để tránh lộ cấu trúc thư mục [cite: x].
    • Mở file cấu hình Database và chỉnh PDO: $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT); (Hoặc ghi lỗi vào file log ẩn thay vì in ra màn hình) [cite: x].

Tổng kết Khóa học & Chặng đường tiếp theo

Chúc mừng bạn! Chúng ta đã hoàn tất toàn bộ chuỗi kiến thức PHP & MySQL Backend [cite: x]. Từ những dòng biến $a, $b thô sơ của những ngày đầu tiên, bạn đã nắm được cách xây dựng một hệ thống phân chia cấu trúc MVC chuẩn mực, biết cách bảo vệ hệ thống khỏi Hacker, và biết cách thiết kế cơ sở dữ liệu có quan hệ [cite: x].

Vậy đi tiếp như thế nào?

PHP thuần (Core PHP) là nền tảng võ công nội công tâm pháp [cite: x]. Bây giờ, khi nội công đã vững, bạn hoàn toàn có thể tự tin cầm các "vũ khí" hạng nặng lên để tăng tốc độ phát triển dự án [cite: x]:

  1. Học Framework: Bắt tay vào học Laravel - Framework PHP mạnh nhất thế giới hiện nay [cite: x]. Bạn sẽ thấy Laravel sử dụng lại toàn bộ các khái niệm MVC, PDO, Session, CSRF, Route mà bạn vừa học, nhưng ở một mức độ tự động hóa tuyệt vời [cite: x].
  2. Xây dựng API (RESTful): Thay vì để PHP trả về giao diện HTML, hãy học cách dùng PHP trả về dữ liệu định dạng JSON để phục vụ cho các ứng dụng Mobile App hoặc các Frontend Framework (React, Vue) [cite: x].
  3. Kiến trúc hệ thống lớn: Tìm hiểu sâu hơn về Redis Cache, Message Queue, Docker và các mẫu thiết kế hướng dịch vụ (Microservices) [cite: x].

All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.