Đây là PHẦN CUỐI CÙNG của loạt bài "Làm website thương mại điện tử bằng PHP 8 + MySQL 9 từ A-Z". Nếu bạn đã dán đủ code từ Phần 1 đến Phần 9, bạn đang có 1 website ShopVN chạy đầy đủ: xem sản phẩm, giỏ hàng, đặt hàng, tài khoản, và trang quản trị. Hôm nay hoàn thiện nốt 2 việc cuối: tối ưu hiệu năng và triển khai lên hosting thật. ============================== 1. PHÂN TRANG (PAGINATION) - TRÁNH TẢI CẢ NGHÌN SẢN PHẨM CÙNG LÚC ============================== Hiện tại index.php chỉ LIMIT 12, category.php lấy hết không giới hạn - khi shop có vài trăm sản phẩm sẽ chậm. Thêm hàm phân trang dùng chung vào includes/functions.php: function paginate_params(int $total_items, int $per_page = 12): array { $page = max(1, (int) ($_GET['page'] ?? 1)); $total_pages = max(1, (int) ceil($total_items / $per_page)); $page = min($page, $total_pages); $offset = ($page - 1) * $per_page; return ['page' => $page, 'total_pages' => $total_pages, 'offset' => $offset, 'per_page' => $per_page]; } Cập nhật category.php (áp dụng tương tự cho index.php) - thay đoạn lấy $products bằng: $per_page = 12; $count_stmt = $pdo->prepare('SELECT COUNT(*) FROM products WHERE category_id = ? AND is_active = 1'); $count_stmt->execute([$category['id']]); $total_items = (int) $count_stmt->fetchColumn(); $p = paginate_params($total_items, $per_page); $stmt = $pdo->prepare( 'SELECT id, name, slug, price, sale_price, image FROM products WHERE category_id = :cid AND is_active = 1 ORDER BY created_at DESC LIMIT :limit OFFSET :offset' ); $stmt->bindValue(':cid', $category['id'], PDO::PARAM_INT); $stmt->bindValue(':limit', $p['per_page'], PDO::PARAM_INT); $stmt->bindValue(':offset', $p['offset'], PDO::PARAM_INT); $stmt->execute(); $products = $stmt->fetchAll(); Và thêm thanh phân trang vào cuối phần hiển thị .product-grid (dán ngay sau thẻ đóng </div> của product-grid): <?php if ($p['total_pages'] > 1): ?> <div style="display:flex; gap:8px; justify-content:center; margin-top:24px;"> <?php for ($i = 1; $i <= $p['total_pages']; $i++): ?> <a href="?slug=<?= e($category['slug']) ?>&page=<?= $i ?>" style="padding:8px 14px; border-radius:4px; background:<?= $i === $p['page'] ? '#e53935' : '#fff' ?>; color:<?= $i === $p['page'] ? '#fff' : '#222' ?>; border:1px solid #ddd;"> <?= $i ?> </a> <?php endfor; ?> </div> <?php endif; ?> Lưu ý quan trọng: khi dùng LIMIT/OFFSET với PDO, phải bindValue() kèm PDO::PARAM_INT thay vì truyền thẳng qua execute([...]) - vì mặc định PDO (khi tắt emulate prepares như ta đã làm ở Phần 3) sẽ truyền tham số dạng chuỗi, mà MySQL không chấp nhận LIMIT là chuỗi, sẽ báo lỗi cú pháp. Áp dụng cách làm tương tự cho admin/products.php và admin/orders.php khi danh sách nhiều lên (SELECT COUNT(*) trước, rồi SELECT ... LIMIT :limit OFFSET :offset, rồi in thanh phân trang). ============================== 2. THÊM INDEX CHO CÁC CỘT HAY TRUY VẤN ============================== Bảng products đã có UNIQUE KEY trên slug và index trên category_id (tự tạo do FOREIGN KEY). Khi shop có nhiều sản phẩm, câu tìm kiếm ở search.php (LIKE '%...%') sẽ chậm dần - đây là giới hạn tự nhiên của LIKE (không tận dụng được index khi có dấu % ở đầu chuỗi). Với website nhỏ/vừa thì không đáng lo, nhưng nếu muốn tối ưu thêm, chạy câu sau để thêm FULLTEXT index cho tìm kiếm nhanh hơn nhiều: ALTER TABLE products ADD FULLTEXT INDEX ft_products_name (name); Sau đó ở search.php, thay câu SELECT bằng: $stmt = $pdo->prepare( 'SELECT id, name, slug, price, sale_price, image FROM products WHERE is_active = 1 AND MATCH(name) AGAINST (:q IN NATURAL LANGUAGE MODE)' ); $stmt->execute([':q' => $q]); FULLTEXT tìm theo TỪ (word-based), nhanh hơn LIKE rất nhiều với bảng lớn, nhưng sẽ không tìm được tìm kiếm theo "chuỗi con nằm giữa từ" như LIKE - với website bán hàng thông thường thì FULLTEXT là đủ dùng và tốt hơn. Ngoài ra thêm index cho cột hay lọc trong admin: ALTER TABLE orders ADD INDEX idx_orders_status (status); ============================== 3. TỐI ƯU ẢNH SẢN PHẨM ============================== - Trước khi upload, nén ảnh bằng công cụ online (TinyPNG, Squoosh) hoặc plugin - ảnh sản phẩm chỉ cần 800x800px là đủ đẹp, không cần ảnh gốc 4000x3000px chụp từ máy ảnh (vừa nặng vừa chậm tải). - Thêm loading="lazy" vào thẻ <img> ở product-grid (index.php, category.php, search.php) để trình duyệt chỉ tải ảnh khi cuộn tới, giảm tải trang lần đầu: <img src="..." loading="lazy" alt="..."> - Nếu muốn chuẩn hơn, có thể dùng thư viện GD (đã liệt kê ở Phần 1) để tự động resize ảnh về kích thước chuẩn ngay khi admin upload trong product_form.php, nhưng với quy mô shop nhỏ thì nén tay trước khi upload là đủ. ============================== 4. CACHE TRÌNH DUYỆT CHO FILE TĨNH (CSS/ẢNH) ============================== Thêm vào .htaccess ở thư mục gốc dự án (tạo mới nếu chưa có) để trình duyệt lưu cache CSS/ảnh, giảm số lần tải lại khi khách quay lại site: <IfModule mod_expires.c> ExpiresActive On ExpiresByType text/css "access plus 1 month" ExpiresByType image/jpeg "access plus 1 month" ExpiresByType image/png "access plus 1 month" ExpiresByType image/webp "access plus 1 month" </IfModule> ============================== 5. TRIỂN KHAI (DEPLOY) LÊN HOSTING THẬT - TỪNG BƯỚC ============================== Giả sử bạn thuê hosting cPanel (phổ biến nhất cho người mới, giá rẻ, có sẵn ở hầu hết nhà cung cấp Việt Nam): Bước 1 - Chuẩn bị: đăng ký hosting hỗ trợ PHP 8.1+ và MySQL 8.0+/9.x, đăng ký tên miền, trỏ tên miền về hosting (nameserver hoặc bản ghi A tuỳ nhà cung cấp hướng dẫn). Bước 2 - Tạo cơ sở dữ liệu: vào cPanel > MySQL Databases, tạo 1 database (VD tên_tk_shopvn), tạo 1 user MySQL riêng, gán user đó vào database vừa tạo với đầy đủ quyền (All Privileges). Ghi lại chính xác: tên database, tên user, mật khẩu. Bước 3 - Import dữ liệu: vào cPanel > phpMyAdmin, chọn database vừa tạo, vào tab Import, chọn file database/shopvn.sql (file ở Phần 2) và bấm Go. Kiểm tra đủ 5 bảng: users, categories, products, orders, order_items. Bước 4 - Upload code: vào cPanel > File Manager (hoặc dùng FileZilla qua FTP), upload TOÀN BỘ thư mục dự án shopvn/ vào đúng thư mục gốc web của tài khoản hosting (thường là public_html/ hoặc public_html/tenmien nếu là subdomain/addon domain). Bước 5 - Sửa lại thông tin kết nối: mở lại config/database.php ngay trên hosting (qua File Manager), sửa 4 dòng đầu thành thông tin THẬT của hosting (không phải 127.0.0.1/root/rỗng như lúc học ở máy cá nhân nữa): $db_host = 'localhost'; // hoặc IP do hosting cung cap $db_name = 'ten_database_that'; $db_user = 'ten_user_that'; $db_pass = 'mat_khau_that'; Bước 6 - Cấp quyền ghi cho thư mục upload: đảm bảo assets/uploads/products/ có quyền ghi (thường chmod 755 hoặc 775 tuỳ hosting) để trang admin upload ảnh sản phẩm hoạt động được. Bước 7 - Tắt debug, bật production: làm theo mục 6 ở Phần 9 (đổi $is_development = false trong config/database.php). Bước 8 - Cài SSL (HTTPS): hầu hết cPanel hiện đại có sẵn "AutoSSL" hoặc Let's Encrypt miễn phí, bật lên trong vài phút. Sau khi có SSL, thêm 'secure' => true vào session_set_cookie_params (mục 2 Phần 9) và cân nhắc thêm đoạn ép chuyển hướng HTTP sang HTTPS trong .htaccess: RewriteEngine On RewriteCond %{HTTPS} off RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] Bước 9 - Đổi mật khẩu admin mặc định: đăng nhập admin/login.php bằng tài khoản admin@shopvn.local, vào sửa lại mật khẩu thật (có thể làm nhanh bằng cách chạy lại đoạn script password_hash() ở Phần 2 rồi UPDATE trực tiếp qua phpMyAdmin, vì loạt bài chưa làm chức năng "đổi mật khẩu" trong giao diện - đây là 1 tính năng hay để bạn tự luyện thêm sau khi hoàn thành series này). Bước 10 - Kiểm tra toàn bộ luồng: đăng ký tài khoản mới, thêm sản phẩm vào giỏ, đặt hàng thử, vào trang admin xác nhận đơn hàng đổi trạng thái, kiểm tra hiển thị trên điện thoại (responsive). ============================== 6. SAO LƯU (BACKUP) ĐỊNH KỲ ============================== Website chạy thật cần backup định kỳ để phòng sự cố: - Database: cPanel thường có sẵn công cụ "Backup Wizard" hoặc lịch tự động export file .sql định kỳ (hàng ngày/hàng tuần tuỳ mức độ hoạt động của shop). - Code + ảnh upload: định kỳ tải toàn bộ thư mục shopvn/ về máy hoặc dùng tính năng backup của hosting. - Nguyên tắc tối thiểu: giữ ít nhất 3 bản backup gần nhất, không lưu backup CÙNG server với website (nếu server sập thì backup cũng mất theo). ============================== 7. TỔNG KẾT TOÀN BỘ LOẠT BÀI ============================== Phần 1: Tổng quan dự án, yêu cầu môi trường, cấu trúc thư mục Phần 2: Thiết kế cơ sở dữ liệu đầy đủ (users, categories, products, orders, order_items) Phần 3: Bộ khung ứng dụng - kết nối PDO, hàm dùng chung, giao diện chung Phần 4: Trang chủ, danh mục, chi tiết sản phẩm Phần 5: Giỏ hàng bằng session Phần 6: Đăng ký / đăng nhập / đăng xuất thành viên Phần 7: Đặt hàng, thanh toán COD, lịch sử đơn hàng (dùng transaction + chống race condition) Phần 8: Trang quản trị - quản lý danh mục, sản phẩm (upload ảnh), đơn hàng Phần 9: Bảo mật toàn diện (CSRF, XSS, SQLi, brute-force, chặn thư mục nhạy cảm, upload an toàn) Phần 10: Tối ưu hiệu năng (phân trang, index, cache) + triển khai lên hosting thật Đi hết 10 phần, bạn không chỉ có 1 website bán hàng chạy được, mà quan trọng hơn là hiểu ĐƯỢC VÌ SAO từng dòng code được viết như vậy - đó mới là thứ giúp bạn tự tin sửa/mở rộng thêm sau này: thêm mã giảm giá (coupon), đánh giá sản phẩm (review), thanh toán online (VNPay/Momo), gửi email xác nhận đơn hàng, v.v. Những tính năng đó đều xây dựng trên đúng nền tảng (PDO + prepared statement + escape output + CSRF + transaction) mà loạt bài đã hướng dẫn. Cảm ơn anh em đã theo dõi hết loạt bài! Có thắc mắc hoặc lỗi phát sinh khi làm theo, cứ để lại bình luận ở từng phần tương ứng, mình sẽ hỗ trợ giải đáp. --- Hết Phần 10 (Hết loạt bài) --- ---------------------------------------- MỤC LỤC LOẠT BÀI: 1. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 1: Tổng Quan Dự Án, Yêu Cầu Môi Trường & Cấu Trúc Thư Mục - https://diendan.anyvlogs.info/chu-de/457-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-1-tong-quan-du-an-yeu-cau-moi-truong-cau-truc-thu-muc 2. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 2: Thiết Kế Cơ Sở Dữ Liệu (Database) Đầy Đủ Cho Website Bán Hàng - https://diendan.anyvlogs.info/chu-de/458-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-2-thiet-ke-co-so-du-lieu-database-day-du-cho-website-ban-hang 3. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 3: Dựng Khung Ứng Dụng - Kết Nối PDO, Hàm Dùng Chung, Giao Diện Chung - https://diendan.anyvlogs.info/chu-de/459-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-3-dung-khung-ung-dung-ket-noi-pdo-ham-dung-chung-giao-dien-chung 4. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 4: Trang Chủ, Danh Mục Sản Phẩm & Chi Tiết Sản Phẩm - https://diendan.anyvlogs.info/chu-de/460-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-4-trang-chu-danh-muc-san-pham-chi-tiet-san-pham 5. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 5: Xây Dựng Giỏ Hàng (Cart) Bằng Session - https://diendan.anyvlogs.info/chu-de/461-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-5-xay-dung-gio-hang-cart-bang-session 6. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 6: Đăng Ký, Đăng Nhập, Đăng Xuất Thành Viên - https://diendan.anyvlogs.info/chu-de/462-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-6-dang-ky-dang-nhap-dang-xuat-thanh-vien 7. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 7: Đặt Hàng, Thanh Toán COD & Lịch Sử Đơn Hàng - https://diendan.anyvlogs.info/chu-de/463-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-7-dat-hang-thanh-toan-cod-lich-su-don-hang 8. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 8: Trang Quản Trị (Admin) - Quản Lý Danh Mục, Sản Phẩm, Đơn Hàng - https://diendan.anyvlogs.info/chu-de/464-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-8-trang-quan-tri-admin-quan-ly-danh-muc-san-pham-don-hang 9. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 9: Bảo Mật Website (CSRF, XSS, SQL Injection, Upload An Toàn) - https://diendan.anyvlogs.info/chu-de/465-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-9-bao-mat-website-csrf-xss-sql-injection-upload-an-toan >> 10. [Series PHP+MySQL] Làm Website Bán Hàng Từ A-Z - Phần 10 (Kết Thúc): Tối Ưu Hiệu Năng & Triển Khai Lên Hosting Thật - https://diendan.anyvlogs.info/chu-de/466-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-10-ket-thuc-toi-uu-hieu-nang-trien-khai-len-hosting-that Phần trước: https://diendan.anyvlogs.info/chu-de/465-series-php-mysql-lam-website-ban-hang-tu-a-z-phan-9-bao-mat-website-csrf-xss-sql-injection-upload-an-toan