1. 11:59 06/08/2026
Tiếp Phần 2 (đã có CSDL). Hôm nay dựng "bộ khung" cho toàn bộ website: kết nối PDO, các hàm dùng chung, và giao diện header/footer. Mọi file .php ở các phần sau đều sẽ include lại các file này, nên làm đúng ngay từ đầu là quan trọng nhất.
==============================
1. FILE config/database.php
==============================
<?php
// Thong tin ket noi CSDL - sua lai theo may cua ban
$db_host = '127.0.0.1';
$db_port = 3306;
$db_name = 'shopvn';
$db_user = 'root';
$db_pass = '';
try {
$pdo = new PDO(
"mysql:host={$db_host};port={$db_port};dbname={$db_name};charset=utf8mb4",
$db_user,
$db_pass,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]
);
} catch (PDOException $e) {
die('Khong ket noi duoc co so du lieu: ' . $e->getMessage());
}
Giải thích 2 dòng ATTR quan trọng:
- PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION: mọi lỗi SQL sẽ ném exception thay vì âm thầm bỏ qua, giúp phát hiện lỗi ngay khi code sai.
- PDO::ATTR_EMULATE_PREPARES => false: bắt PHP dùng prepared statement THẬT của MySQL (không giả lập), đây là lớp bảo vệ chống SQL Injection hiệu quả nhất.
==============================
2. FILE includes/functions.php
==============================
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/../config/database.php';
// Chong XSS: luon dung ham nay khi in du lieu tu CSDL/nguoi dung ra HTML
function e(?string $value): string
{
return htmlspecialchars($value ?? '', ENT_QUOTES, 'UTF-8');
}
// Dinh dang so tien kieu Viet Nam: 129000 -> "129.000 d"
function format_price(int|float $price): string
{
return number_format((float) $price, 0, ',', '.') . ' d';
}
// Chuyen chuoi tieng Viet co dau thanh slug an toan cho URL
function slugify(string $text): string
{
$text = trim($text);
$map = [
'à'=>'a','á'=>'a','ạ'=>'a','ả'=>'a','ã'=>'a','â'=>'a','ầ'=>'a','ấ'=>'a','ậ'=>'a','ẩ'=>'a','ẫ'=>'a','ă'=>'a','ằ'=>'a','ắ'=>'a','ặ'=>'a','ẳ'=>'a','ẵ'=>'a',
'è'=>'e','é'=>'e','ẹ'=>'e','ẻ'=>'e','ẽ'=>'e','ê'=>'e','ề'=>'e','ế'=>'e','ệ'=>'e','ể'=>'e','ễ'=>'e',
'ì'=>'i','í'=>'i','ị'=>'i','ỉ'=>'i','ĩ'=>'i',
'ò'=>'o','ó'=>'o','ọ'=>'o','ỏ'=>'o','õ'=>'o','ô'=>'o','ồ'=>'o','ố'=>'o','ộ'=>'o','ổ'=>'o','ỗ'=>'o','ơ'=>'o','ờ'=>'o','ớ'=>'o','ợ'=>'o','ở'=>'o','ỡ'=>'o',
'ù'=>'u','ú'=>'u','ụ'=>'u','ủ'=>'u','ũ'=>'u','ư'=>'u','ừ'=>'u','ứ'=>'u','ự'=>'u','ử'=>'u','ữ'=>'u',
'ỳ'=>'y','ý'=>'y','ỵ'=>'y','ỷ'=>'y','ỹ'=>'y',
'đ'=>'d',
];
$lower = mb_strtolower($text, 'UTF-8');
$ascii = strtr($lower, $map);
$ascii = preg_replace('/[^a-z0-9]+/u', '-', $ascii);
$ascii = trim($ascii, '-');
return $ascii === '' ? 'san-pham' : $ascii;
}
function redirect(string $path): void
{
header('Location: ' . $path);
exit;
}
function is_logged_in(): bool
{
return isset($_SESSION['user_id']);
}
function is_admin(): bool
{
return is_logged_in() && ($_SESSION['user_role'] ?? '') === 'admin';
}
function require_login(): void
{
if (!is_logged_in()) {
redirect('login.php');
}
}
function require_admin(): void
{
if (!is_admin()) {
// Ham nay chi duoc goi tu cac file trong thu muc admin/, nen duong dan
// tuong doi "login.php" se tro dung ve admin/login.php (Phan 8)
redirect('login.php');
}
}
// So luong san pham dang co trong gio hang (session)
function cart_count(): int
{
$count = 0;
foreach ($_SESSION['cart'] ?? [] as $qty) {
$count += (int) $qty;
}
return $count;
}
// ----- CSRF: chong gia mao form tu trang khac -----
function csrf_token(): string
{
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function csrf_field(): string
{
return '<input type="hidden" name="csrf_token" value="' . csrf_token() . '">';
}
function csrf_verify(): void
{
$token = $_POST['csrf_token'] ?? '';
if (!hash_equals($_SESSION['csrf_token'] ?? '', $token)) {
http_response_code(400);
die('Phien lam viec khong hop le, vui long tai lai trang va thu lai.');
}
}
Từ Phần 4 trở đi, MỌI file .php trong dự án sẽ bắt đầu bằng dòng:
require_once __DIR__ . '/includes/functions.php';
(hoặc '../includes/functions.php' đối với file nằm trong thư mục admin/) - dòng này tự động include luôn config/database.php nên có sẵn biến $pdo để dùng.
==============================
3. FILE includes/header.php
==============================
<?php
// File nay duoc include boi cac trang o thu muc goc, nen duong dan anh/css la truc tiep
?>
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= isset($page_title) ? e($page_title) . ' - ShopVN' : 'ShopVN - Mua sam truc tuyen' ?></title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<header class="site-header">
<div class="container header-inner">
<a href="index.php" class="logo">Shop<span>VN</span></a>
<form action="search.php" method="get" class="search-form">
<input type="text" name="q" placeholder="Tim san pham..." value="<?= e($_GET['q'] ?? '') ?>">
<button type="submit">Tim</button>
</form>
<nav class="header-nav">
<a href="cart.php" class="cart-link">Gio hang (<?= cart_count() ?>)</a>
<?php if (is_logged_in()): ?>
<a href="account.php"><?= e($_SESSION['user_name']) ?></a>
<a href="logout.php">Dang xuat</a>
<?php else: ?>
<a href="login.php">Dang nhap</a>
<a href="register.php">Dang ky</a>
<?php endif; ?>
</nav>
</div>
</header>
<main class="container">
==============================
4. FILE includes/footer.php
==============================
</main>
<footer class="site-footer">
<div class="container">
<p>© <?= date('Y') ?> ShopVN - Website demo hoc lap trinh PHP + MySQL.</p>
</div>
</footer>
</body>
</html>
==============================
5. FILE assets/css/style.css (giao diện cơ bản, responsive)
==============================
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: Arial, Helvetica, sans-serif; background: #f5f6fa; color: #222; line-height: 1.6; }
.container { max-width: 1140px; margin: 0 auto; padding: 0 16px; }
a { text-decoration: none; color: inherit; }
.site-header { background: #fff; box-shadow: 0 1px 4px rgba(0,0,0,.08); position: sticky; top: 0; z-index: 10; }
.header-inner { display: flex; align-items: center; gap: 20px; padding: 14px 16px; flex-wrap: wrap; }
.logo { font-size: 22px; font-weight: bold; color: #e53935; }
.logo span { color: #222; }
.search-form { flex: 1; display: flex; min-width: 200px; }
.search-form input { flex: 1; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px 0 0 4px; }
.search-form button { padding: 8px 16px; background: #e53935; color: #fff; border: none; border-radius: 0 4px 4px 0; cursor: pointer; }
.header-nav { display: flex; gap: 14px; align-items: center; font-size: 14px; }
.header-nav a { padding: 6px 8px; }
.header-nav a:hover { color: #e53935; }
main.container { padding: 24px 16px; min-height: 60vh; }
.page-title { font-size: 22px; margin-bottom: 16px; }
.category-bar { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px; }
.category-bar a { background: #fff; padding: 8px 16px; border-radius: 20px; border: 1px solid #ddd; font-size: 14px; }
.category-bar a.active, .category-bar a:hover { background: #e53935; color: #fff; border-color: #e53935; }
.product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 18px; }
.product-card { background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 4px rgba(0,0,0,.08); transition: transform .15s; }
.product-card:hover { transform: translateY(-3px); box-shadow: 0 4px 12px rgba(0,0,0,.12); }
.product-card img { width: 100%; height: 180px; object-fit: cover; }
.product-card .info { padding: 12px; }
.product-card .name { font-size: 14px; font-weight: 600; min-height: 40px; }
.price { color: #e53935; font-weight: bold; }
.price-old { color: #999; text-decoration: line-through; font-size: 13px; margin-left: 6px; }
.btn { display: inline-block; padding: 10px 20px; background: #e53935; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; }
.btn:hover { background: #c62828; }
.btn-outline { background: #fff; color: #e53935; border: 1px solid #e53935; }
.form-box { background: #fff; max-width: 420px; margin: 0 auto; padding: 24px; border-radius: 8px; box-shadow: 0 1px 4px rgba(0,0,0,.08); }
.form-group { margin-bottom: 14px; }
.form-group label { display: block; margin-bottom: 6px; font-size: 14px; font-weight: 600; }
.form-group input, .form-group textarea, .form-group select { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
.alert { padding: 10px 14px; border-radius: 4px; margin-bottom: 14px; font-size: 14px; }
.alert-error { background: #fdecea; color: #c62828; }
.alert-success { background: #e8f5e9; color: #2e7d32; }
table.data-table { width: 100%; border-collapse: collapse; background: #fff; }
table.data-table th, table.data-table td { padding: 10px 12px; border-bottom: 1px solid #eee; text-align: left; font-size: 14px; }
table.data-table th { background: #fafafa; }
.site-footer { text-align: center; padding: 20px; color: #777; font-size: 13px; }
@media (max-width: 640px) {
.header-inner { flex-direction: column; align-items: stretch; }
}
Vậy là xong bộ khung. Từ Phần 4, mọi trang chỉ cần include header/footer là có ngay giao diện đồng bộ, không phải lặp lại HTML. Phần 4 sẽ code trang chủ (index.php), trang danh mục (category.php) và trang chi tiết sản phẩm (product.php) - bắt đầu hiển thị dữ liệu thật từ CSDL lên giao diện. Hẹn gặp lại!
--- Hết Phần 3 ---
----------------------------------------
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/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
Phần sau: 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