MVC (Model–View–Controller) tách ứng dụng thành ba phần: dữ liệu, giao diện và điều phối. Bài này giải thích vai trò từng phần, luồng xử lý một yêu cầu, rồi xây dựng một ứng dụng MVC nhỏ bằng PHP thuần với router, model, controller và view.
- Hiểu vai trò Model, View, Controller và luồng request
- Tổ chức thư mục dự án theo MVC
- Viết router đơn giản dựa trên URL
- Hiểu ưu, nhược điểm và vị trí của MVC trong các framework
- Model
- Dữ liệu và quy tắc nghiệp vụ; làm việc với CSDL.
- View
- Sinh HTML/JSON từ dữ liệu; không truy cập CSDL.
- Controller
- Nhận yêu cầu, gọi Model, chọn View.
- Router
- Phân tích URL để gọi đúng Controller.
- Front controller
- index.php duy nhất nhận mọi yêu cầu.
Khái niệm và lý thuyết
Khi mã nằm hết trong một tệp (SQL lẫn HTML lẫn xử lý form), dự án nhanh chóng khó sửa. MVC đặt ra kỷ luật: Model trả lời câu hỏi nghiệp vụ (“lấy 10 sinh viên điểm cao nhất”), View chỉ hiển thị, Controller làm cầu nối. Nhờ đó người làm giao diện và người viết nghiệp vụ làm song song, mã dễ kiểm thử, dễ thêm View mới (bản di động, JSON).
Luồng xử lý: trình duyệt gửi GET /sinhvien/SV001 → Router trong index.php xác định SinhVienController::chiTiet("SV001") → Controller kiểm tra tham số, gọi SinhVien::find() → Model truy vấn CSDL, trả dữ liệu → Controller nạp View kèm dữ liệu → View sinh HTML → trả về trình duyệt.
Cấu trúc thư mục
htdocs/qlsv-mvc/
├── index.php # front controller + router
├── .htaccess # chuyển mọi URL về index.php
├── config/database.php
├── core/Database.php # kết nối dùng chung
├── models/SinhVien.php
├── controllers/SinhVienController.php
└── views/
├── layout.php
└── sinhvien/danh_sach.php, chi_tiet.php, form.php# .htaccess (Apache, bật mod_rewrite)
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]Model
<?php
// core/Database.php
class Database {
private static ?mysqli $conn = null;
public static function get(): mysqli {
if (!self::$conn) {
$c = require __DIR__ . "/../config/database.php";
self::$conn = new mysqli($c["host"], $c["user"], $c["pass"], $c["db"]);
self::$conn->set_charset("utf8mb4");
}
return self::$conn;
}
}
// models/SinhVien.php
class SinhVien {
public static function all(): array {
return Database::get()->query("SELECT * FROM sinhvien ORDER BY ho_ten")->fetch_all(MYSQLI_ASSOC);
}
public static function find(string $ma): ?array {
$st = Database::get()->prepare("SELECT * FROM sinhvien WHERE ma_sv = ?");
$st->bind_param("s", $ma); $st->execute();
return $st->get_result()->fetch_assoc() ?: null;
}
public static function create(array $d): bool {
$st = Database::get()->prepare("INSERT INTO sinhvien (ma_sv, ho_ten, lop, email) VALUES (?, ?, ?, ?)");
$st->bind_param("ssss", $d["ma_sv"], $d["ho_ten"], $d["lop"], $d["email"]);
return $st->execute();
}
}Controller
<?php
// controllers/SinhVienController.php
class SinhVienController {
public function index(): void {
$ds = SinhVien::all();
$this->render("sinhvien/danh_sach", ["ds" => $ds, "tieu_de" => "Danh sách sinh viên"]);
}
public function chiTiet(string $ma): void {
$sv = SinhVien::find($ma);
if (!$sv) { http_response_code(404); exit("Không tìm thấy sinh viên"); }
$this->render("sinhvien/chi_tiet", ["sv" => $sv, "tieu_de" => $sv["ho_ten"]]);
}
public function them(): void {
$loi = [];
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$d = array_map("trim", $_POST);
if ($d["ho_ten"] === "") $loi[] = "Thiếu họ tên";
if (!$loi && SinhVien::create($d)) { header("Location: /qlsv-mvc/sinhvien", true, 303); exit; }
}
$this->render("sinhvien/form", ["loi" => $loi, "tieu_de" => "Thêm sinh viên"]);
}
private function render(string $view, array $data = []): void {
extract($data); // biến $ds, $sv... cho view
ob_start(); require __DIR__ . "/../views/$view.php"; $noi_dung = ob_get_clean();
require __DIR__ . "/../views/layout.php"; // layout dùng $noi_dung, $tieu_de
}
}View và layout
<?php /* views/layout.php */ ?>
<!DOCTYPE html><html lang="vi"><head><meta charset="UTF-8"><title><?= htmlspecialchars($tieu_de) ?></title></head>
<body><nav><a href="/qlsv-mvc/sinhvien">Sinh viên</a> | <a href="/qlsv-mvc/sinhvien/them">Thêm</a></nav>
<?= $noi_dung ?>
</body></html>
<?php /* views/sinhvien/danh_sach.php */ ?>
<h1><?= htmlspecialchars($tieu_de) ?></h1>
<table border="1">
<?php foreach ($ds as $sv): ?>
<tr><td><a href="/qlsv-mvc/sinhvien/<?= urlencode($sv["ma_sv"]) ?>"><?= htmlspecialchars($sv["ma_sv"]) ?></a></td>
<td><?= htmlspecialchars($sv["ho_ten"]) ?></td><td><?= htmlspecialchars($sv["lop"]) ?></td></tr>
<?php endforeach; ?>
</table>Router (index.php)
<?php
declare(strict_types=1);
spl_autoload_register(function ($lop) {
foreach (["core", "models", "controllers"] as $tm) {
$f = __DIR__ . "/$tm/$lop.php"; if (is_file($f)) { require $f; return; }
}
});
$duong_dan = trim(str_replace("/qlsv-mvc", "", parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH)), "/");
$routes = [
"GET sinhvien" => ["SinhVienController", "index"],
"GET sinhvien/them" => ["SinhVienController", "them"],
"POST sinhvien/them" => ["SinhVienController", "them"],
"GET sinhvien/{ma}" => ["SinhVienController", "chiTiet"],
];
foreach ($routes as $mau => [$ctrl, $pt]) {
[$method, $pattern] = explode(" ", $mau, 2);
$regex = "#^" . preg_replace("#\{(\w+)\}#", "([^/]+)", $pattern) . "$#";
if ($_SERVER["REQUEST_METHOD"] === $method && preg_match($regex, $duong_dan, $m)) {
array_shift($m);
(new $ctrl)->$pt(...$m);
exit;
}
}
http_response_code(404); echo "Không tìm thấy trang";Ưu, nhược điểm
| Ưu điểm | Hạn chế |
|---|---|
| Tách biệt mối quan tâm, làm việc song song | Nhiều tệp hơn với ứng dụng rất nhỏ |
| Dễ kiểm thử Model độc lập | Người mới dễ rối vì yêu cầu đi qua nhiều lớp |
| Thêm View mới (JSON, di động) không sửa nghiệp vụ | Dễ vi phạm ranh giới (SQL trong View) nếu thiếu kỷ luật |
| Mở rộng bằng cách thêm C + M + V | Cần router và cấu hình máy chủ (.htaccess) |
Laravel, Symfony, CodeIgniter đều là MVC với router, ORM và template engine làm sẵn; nắm MVC thuần là nắm được “bộ xương” của mọi framework.
Giữ Controller mỏng: kiểm tra đầu vào và điều phối thôi; logic nghiệp vụ (tính điểm, xét tốt nghiệp) đặt trong Model hoặc lớp Service riêng.
Tuyệt đối không viết SQL trong View và không echo HTML trong Model: hai lỗi này làm mất toàn bộ lợi ích của MVC.
- Dựng khung qlsv-mvc như trên và chạy được danh sách, chi tiết sinh viên (CSDL ở loạt PHP và MySQL).
- Thêm route sửa và xóa sinh viên.
- Thêm View JSON: /qlsv-mvc/api/sinhvien trả về json_encode(SinhVien::all()).
- Thêm lớp Request bọc $_GET/$_POST và lớp View có hàm e() để view gọn hơn.
- Model dữ liệu, View hiển thị, Controller điều phối, Router chọn Controller.
- index.php là điểm vào duy nhất (front controller) nhờ .htaccess.
- render() + layout + extract giúp View gọn.
- MVC là nền của Laravel, Symfony, CodeIgniter.