單入口模式是現代 PHP 框架 (Laravel, Symfony, ThinkPHP, Yii) 的核心架構。本文解析一個輕量級自製路由器的實現原理:.htaccess 重寫、單入口 index.php、靜態類路由器、模組控制器約定、跨模組調用、個人主頁短網址支援,並對比主流框架路由機制給出遷移建議。
請求 → Apache/Nginx → .htaccess 重寫所有非靜態請求至 index.php
↓
index.php (單入口)
↓
引入 Rewrite 類 (自動載入/require)
↓
Rewrite::__config(基礎URL, 模組物理路徑, 允許系統函數)
↓
Rewrite::__parse() 核心解析
↓
解析 REQUEST_URI → 模組名 + 方法名
↓
載入模組檔案 (modules/ModuleName.php)
↓
實例化/靜態調用 Rw_ModuleName::method()
↓
執行 init() (前置鉤子) → 執行目標方法 → 輸出回應
# .htaccess (Apache)
RewriteEngine On
# 靜態資源直接服務,不經 PHP
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule !\.(js|ico|gif|jpg|png|css|swf|htm|txt|woff|woff2|svg|map)$ index.php [L]
# PHP 環境安全
php_flag magic_quotes_gpc Off
php_flag register_globals Off
# Nginx 對應配置
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
<?php
/**
* 輕量級單入口路由器
* 特點:靜態方法、無依賴、模組約定、跨模組調用、短網址支援
*/
class Rewrite
{
// 公開靜態屬性
public static $debug = false;
public static $version = '2.2';
public static $pretag = 'Rw_'; // 模組類前綴
public static $linktag = 'index'; // 當前解析路徑標記 (供視圖/菜單高亮)
public static $uid = 0; // 短網址用戶 ID
private static $allowSysFun = []; // 允許直接調用的系統函數 (如 phpinfo)
// 內部狀態
private static $physicalPath = ''; // 模組檔案物理目錄
private static $websitePath = ''; // 站點基礎 URL (含子目錄)
private static $timeStart = 0;
private static $timeEnd = 0;
private static $timePass = 0;
// === 配置入口 ===
public static function __config(string $websitePath, string $physicalPath, array $allowSysFun = []): void
{
self::$websitePath = rtrim($websitePath, '/') . '/';
self::$physicalPath = rtrim($physicalPath, '/') . '/';
self::$allowSysFun = $allowSysFun;
}
// === 核心解析入口 ===
public static function __parse(string $url = ''): void
{
self::$timeStart = self::microtimeFloat();
$requestUri = $url ?: ($_SERVER['REQUEST_URI'] ?? '/');
$parsed = self::parseRequest($requestUri);
// 子域名/短網址模式 (如 user.example.com → profile)
if ($parsed['isSubdomain']) {
self::handleSubdomain($parsed['subdomain'], $parsed['path']);
return;
}
// 系統函數直通 (phpinfo 等)
if ($parsed['method'] && in_array($parsed['method'], self::$allowSysFun, true)) {
$parsed['method']();
return;
}
// 解析模組與方法
$moduleName = $parsed['module'] ?: 'index';
$methodName = $parsed['method'] ?: 'index';
$className = self::$pretag . ucfirst($moduleName);
$filePath = self::$physicalPath . $moduleName . '.php';
// 載入模組檔案
if (!self::loadModule($filePath, $className)) {
self::fallback($moduleName, $methodName);
return;
}
// 執行前置鉤子 init()
if ($methodName !== 'init' && method_exists($className, 'init')) {
$className::init();
}
// 執行目標方法
if (method_exists($className, $methodName)) {
self::$linktag = $moduleName . '/' . $methodName;
$className::$methodName();
} elseif (method_exists($className, 'index')) {
self::$linktag = $moduleName . '/';
$className::index();
} else {
self::fallback($moduleName, $methodName);
}
self::$timeEnd = self::microtimeFloat();
self::$timePass = round((self::$timeEnd - self::$timeStart) * 1000, 3);
if (self::$debug) self::debugInfo();
}
// === 內部解析邏輯 ===
private static function parseRequest(string $uri): array
{
$parsedUrl = parse_url($uri);
$path = $parsedUrl['path'] ?? '/';
$host = $_SERVER['HTTP_HOST'] ?? '';
$baseHost = parse_url(self::$websitePath, PHP_URL_HOST) ?? '';
// 移除站點基礎路徑
$basePath = parse_url(self::$websitePath, PHP_URL_PATH) ?? '/';
if ($basePath !== '/') {
$path = preg_replace('|^' . preg_quote($basePath, '|') . '|', '', $path);
}
// 子域名檢測 (非 www、非基礎域名)
$isSubdomain = false;
$subdomain = '';
if (substr_count($host, '.') >= 2) {
$parts = explode('.', $host);
$subdomain = $parts[0];
if ($subdomain !== 'www' && $subdomain !== $baseHost) {
$isSubdomain = true;
}
}
// 路徑分段
$segments = array_filter(explode('/', trim($path, '/')));
$method = array_pop($segments) ?? 'index';
$module = $segments ? implode('/', $segments) : '';
// 雙下劃線前綴兼容 (舊版 __method)
if (str_starts_with($method, '__')) {
$method = substr($method, 2);
}
return compact('module', 'method', 'isSubdomain', 'subdomain', 'path');
}
// === 模組載入 ===
private static function loadModule(string $filePath, string $className): bool
{
if (!file_exists($filePath)) return false;
require_once $filePath;
return class_exists($className, false);
}
// === 兜底策略 ===
private static function fallback(string $module, string $method): void
{
// 1. 嘗試模組/index
if ($module !== 'index') {
$indexFile = self::$physicalPath . $module . '/index.php';
if (file_exists($indexFile)) {
require_once $indexFile;
$className = self::$pretag . 'Index';
if (class_exists($className) && method_exists($className, 'index')) {
self::$linktag = $module . '/';
$className::index();
return;
}
}
}
// 2. 嘗試全域 index
$globalIndex = self::$physicalPath . 'index.php';
if (file_exists($globalIndex)) {
require_once $globalIndex;
$className = self::$pretag . 'Index';
if (class_exists($className) && method_exists($className, 'index')) {
self::$linktag = 'index/';
$className::index();
return;
}
}
// 3. 404
http_response_code(404);
if (self::$debug) {
echo "404 Not Found
Module: $module, Method: $method
";
} else {
include self::$physicalPath . '404.php'; // 自定義 404 頁
}
}
// === 子域名/短網址處理 ===
private static function handleSubdomain(string $subdomain, string $path): void
{
// 這裡可對接資料庫查詢 subdomain 映射
// 示例:數字子域名 → 用戶主頁
if (is_numeric($subdomain)) {
self::$uid = (int)$subdomain;
$_GET['uid'] = self::$uid;
$module = 'profile';
$method = 'index';
$filePath = self::$physicalPath . $module . '.php';
if (self::loadModule($filePath, self::$pretag . ucfirst($module))) {
$className = self::$pretag . ucfirst($module);
if (method_exists($className, 'init')) $className::init();
$className::$method();
return;
}
}
// 通用映射表 (需自行實現資料庫查詢)
header('Location: ' . self::$websitePath);
exit;
}
// === 跨模組內部調用 ===
public static function __parseUrl(string $url, string $method, $data = null): mixed
{
// 內部調用:Rewrite::__parseUrl('/api/user', 'getInfo', ['id' => 1])
$filePath = self::$physicalPath . ltrim($url, '/') . '.php';
if (!file_exists($filePath)) return null;
require_once $filePath;
$moduleName = basename($url);
$className = self::$pretag . ucfirst($moduleName);
if (class_exists($className) && method_exists($className, $method)) {
self::$linktag = $moduleName . '/' . $method;
return $className::$method($data);
}
return null;
}
// === 工具方法 ===
private static function microtimeFloat(): float
{
[$usec, $sec] = explode(' ', microtime());
return (float)$usec + (float)$sec;
}
public static function __debug(bool $on = true): void { self::$debug = $on; }
private static function debugInfo(): void { echo "Debug: " . print_r([...], true) . "
"; }
}
?>
<?php
// modules/Article.php
class Rw_Article extends Rewrite
{
// 前置鉤子:權限檢查、面包屑、側邊欄數據
public static function init(): void
{
// 例如:檢查登入、設置模組共享變數
if (!isset($_SESSION['user_id'])) {
header('Location: ' . self::$websitePath . 'login');
exit;
}
self::$linktag = 'article/'; // 供佈局高亮
}
// 預設動作:列表頁
public static function index(): void
{
$page = (int)($_GET['page'] ?? 1);
$articles = ArticleModel::paginate($page, 20);
include VIEW_PATH . 'article/list.php';
}
// 詳情頁:/article/detail/123
public static function detail(): void
{
$id = (int)($_GET['id'] ?? 0);
if (!$id) { self::fallback('article', 'detail'); return; }
$article = ArticleModel::find($id);
if (!$article) { http_response_code(404); include VIEW_PATH . '404.php'; return; }
include VIEW_PATH . 'article/detail.php';
}
// AJAX 介面:/article/like
public static function like(): void
{
header('Content-Type: application/json');
$id = (int)($_POST['id'] ?? 0);
echo json_encode(['success' => ArticleModel::like($id)]);
}
}
?>
project/ ├── .htaccess ├── index.php # 單入口 ├── config.php # 配置 ├── core/ │ └── Rewrite.php # 路由器核心 ├── modules/ # 模組控制器 (對應 URL 第一級) │ ├── Index.php # 首頁 │ ├── Article.php # 文章模組 │ ├── User.php # 用戶模組 │ ├── Profile.php # 個人主頁 │ └── Api.php # API 模組 ├── models/ # 資料模型 ├── views/ # 視圖模板 └── public/ # 靜態資源 (css, js, images)
<?php
// 在 Article 模組中調用 User 模組獲取作者資訊
class Rw_Article extends Rewrite
{
public static function detail(): void
{
$authorId = 123;
// 內部調用,不經過 HTTP
$author = self::__parseUrl('/user', 'getProfile', ['id' => $authorId]);
// $author 為 User::getProfile() 返回值
include VIEW_PATH . 'article/detail.php';
}
}
// modules/User.php
class Rw_User extends Rewrite
{
public static function getProfile(array $data): array
{
return UserModel::profile($data['id']);
}
}
?>
<?php
// index.php
Rewrite::__config(
'https://example.com/',
__DIR__ . '/modules/',
['phpinfo', 'phpcredits'] // 允許直接訪問 /phpinfo, /phpcredits
);
Rewrite::__parse();
?>
// 子域名模式:user.example.com → 解析為 profile 模組
// 路徑模式:example.com/123 → 判斷首段為純數字 → 映射為 profile/index?uid=123
// 實現見 handleSubdomain() 與 fallback() 中的數字判斷邏輯
<?php
// 開啟調試
Rewrite::__debug(true);
// 獲取解析耗時
Rewrite::__parse();
echo "Route parse time: " . Rewrite::$timePass . " ms";
?>
| 特性 | 自製 Rewrite | Laravel | Symfony | ThinkPHP 8 |
|---|---|---|---|---|
| 路由定義 | 約定式 (檔案/類/方法) | 路由表 (routes/web.php) + 註解/屬性 | YAML/XML/PHP/屬性 + 路由集合 | 路由表 + 註解/屬性 |
| 參數綁定 | 手動 $_GET/$_POST | 自動依賴注入、模型綁定 | 參數解析器、參數轉換器 | 變數規則、類型轉換 |
| 中介軟體 | init() 鉤子手動調用 | Kernel + 中介軟體管道 | Event Listener / Middleware | 中介軟體管道 |
| 分組/前綴/域名 | 目錄結構/子域名解析 | Route::group/domain/prefix | RouteCollection / Host 匹配 | 群組路由/域名路由 |
| 反向生成 URL | 手動拼接 | route() / url() helper | generateUrl() | url() / route() |
| 快取路由 | 無 (每請求解析) | route:cache (生產必開) | RouterCacheWarmer | 路由快取 |
| 學習曲線 | 極低 | 中 | 高 | 低 |
| 擴展性 | 有限 | 極強 (宏、自定義綁定) | 極強 (編譯器、擴展點) | 強 (擴展、行為) |
#[Route('/article/{id}', methods: ['GET'])] 取代約定式自製路由器是理解框架核心機制的最佳途徑。本文實現展示了:
init() 實現橫切關注點 (權限、佈局變數)__parseUrl() 打破模組隔離,服務複用隨著專案增長,建議漸進式引入成熟路由組件,而非一次性重寫。核心原理通透,遷移自然水到渠成。