MVC(Model-View-Controller)是 Web 應用最經典的架構模式。本文不談具體框架,而是從「為什麼需要 MVC」「核心職責劃分」「PHP 自建 MVC 最小骨架」「請求生命週期」「常見誤區與演進方向」五個維度,幫你建立可遷移的架構思維。
| 痛點 | 無 MVC(單腳本) | 有 MVC |
|---|---|---|
| 代碼組織 | 所有邏輯塞在 page.php,千行巨石 | 職責分離,檔案小、易定位 |
| 維護性 | 改樣式怕動 SQL,改邏輯怕壞 HTML | 修改 View 不動 Model,改 Model 不動 Controller |
| 複用性 | 複製貼上,Bug 同步傳播 | Model/Service 複用,Controller 組合 |
| 測試性 | 難以單元測試 | Model/Service 可獨立測試,Controller 可 Mock |
| 團隊協作 | 衝突不斷,無法並行 | 前後端分離,後端並行開發不同 Module |
app/
├── Core/ # 核心基類
│ ├── Application.php # 應用啟動、路由分發
│ ├── Controller.php # Controller 基類
│ ├── Model.php # Model 基類(含 DB 連線)
│ └── View.php # View 基類(含模板引擎)
├── Controller/ # 業務控制器
│ └── UserController.php
├── Model/ # 業務模型
│ └── UserModel.php
├── View/ # 模板檔案
│ └── user/
│ ├── list.php
│ └── detail.php
├── Config/
│ └── database.php
└── public/
└── index.php # 單一入口
<?php
// Core/Application.php
class Application {
public function run(): void {
// 1. 解析路由
$route = $this->parseRoute();
// 2. 實例化 Controller
$controllerClass = "App\\Controller\\{$route['controller']}Controller";
$controller = new $controllerClass();
// 3. 呼叫 Action
$action = $route['action'] . 'Action';
$controller->$action($route['params']);
}
private function parseRoute(): array {
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$parts = array_filter(explode('/', trim($uri, '/')));
return [
'controller' => $parts[0] ?? 'Index',
'action' => $parts[1] ?? 'index',
'params' => array_slice($parts, 2),
];
}
}
// Core/Controller.php
class Controller {
protected function model(string $name): object {
$class = "App\\Model\\{$name}Model";
return new $class();
}
protected function view(string $template, array $data = []): void {
extract($data);
include __DIR__ . "/../../View/{$template}.php";
}
protected function json(array $data, int $code = 200): void {
http_response_code($code);
header('Content-Type: application/json');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
}
// Core/Model.php
class Model {
protected PDO $db;
public function __construct() {
$config = require __DIR__ . '/../Config/database.php';
$this->db = new PDO(
"mysql:host={$config['host']};dbname={$config['dbname']};charset=utf8mb4",
$config['user'], $config['pass'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
}
}
// Core/View.php (若引入模板引擎如 Twig/Blade,替換此處)
class View {
public static function render(string $template, array $data = []): string {
extract($data);
ob_start();
include __DIR__ . "/../View/{$template}.php";
return ob_get_clean();
}
}
<?php
// Model/UserModel.php
class UserModel extends Model {
public function getById(int $id): ?array {
$stmt = $this->db->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}
public function getList(int $page = 1, int $size = 20): array {
$offset = ($page - 1) * $size;
$stmt = $this->db->prepare('SELECT * FROM users LIMIT ? OFFSET ?');
$stmt->bindValue(1, $size, PDO::PARAM_INT);
$stmt->bindValue(2, $offset, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
// Controller/UserController.php
class UserController extends Controller {
public function indexAction(array $params): void {
$page = (int)($_GET['page'] ?? 1);
$users = $this->model('User')->getList($page);
$this->view('user/list', ['users' => $users, 'page' => $page]);
}
public function detailAction(array $params): void {
$id = (int)($params[0] ?? 0);
$user = $this->model('User')->getById($id);
if (!$user) {
$this->json(['error' => 'User not found'], 404);
return;
}
$this->view('user/detail', ['user' => $user]);
}
}
// View/user/list.php
<h1>用戶列表 (第 = $page ?> 頁) </h1>
<ul>
<li><a href="/user/detail/= $u['id'] ?>">= htmlspecialchars($u['name']) ?></a></li>
</ul>
// View/user/detail.php
<h1>= htmlspecialchars($user['name']) ?></h1>
<p>Email: = htmlspecialchars($user['email']) ?></p>
<p>註冊時間: = $user['created_at'] ?></p>
<?php
// public/index.php
require_once __DIR__ . '/../vendor/autoload.php'; // 或手動 require Core 檔案
$app = new App\Core\Application();
$app->run();
HTTP Request
│
▼
public/index.php (單一入口)
│
▼
Application::run()
│
├── parseRoute() → Controller/Action/Params
│
▼
new UserController()
│
├── model('User') → new UserModel() → PDO 查詢
│
▼
view('user/list', $data)
│
├── extract($data)
│
▼
include View/user/list.php → HTML 輸出
│
▼
HTTP Response
| 誤區 | 後果 | 修正 |
|---|---|---|
| Controller 寫 SQL | 業務邏輯分散、難測試、無法複用 | 下沉至 Model/Service/Repository |
| View 直接查 DB | 模板耦合資料層、安全風險、效能不可控 | Controller 準備好所有資料再傳給 View |
| Model 返回 HTML/JSON | 模型職責不純、難以適配多端 | Model 只返回純資料(陣列/DTO/Entity) |
| God Controller(萬能控制器) | 單檔案數千行、耦合度極高 | 拆分為多個 Controller,抽象 Service 層 |
| 忽略輸出逃逸 | XSS 漏洞 | 模板引擎自動逃逸 / 手動 htmlspecialchars |
MVC 不是框架專利,而是「關注點分離」的工程智慧。理解其本質,無論用何種工具,都能寫出清晰、可維護、可演進的代碼。