首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 快速 URL 重寫路由實現:單入口、靜態方法、模組化控制器與跨模組調用

     編輯:PHP 相關知識

單入口模式是現代 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 重寫規則

# .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;
}

三、核心路由器類 (Rewrite) 完整實現

<?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) . "
"; } } ?>

四、模組控制器開發規範

1. 基礎模組結構

<?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)]);
    }
}
?>

2. 目錄結構建議

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)

五、進階特性詳解

1. 跨模組調用:模組間服務複用

<?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']);
    }
}
?>

2. 系統函數直通:調試/工具頁

<?php
// index.php
Rewrite::__config(
    'https://example.com/',
    __DIR__ . '/modules/',
    ['phpinfo', 'phpcredits'] // 允許直接訪問 /phpinfo, /phpcredits
);
Rewrite::__parse();
?>

3. 短網址/個人主頁:user.example.com 或 example.com/123


// 子域名模式:user.example.com → 解析為 profile 模組
// 路徑模式:example.com/123 → 判斷首段為純數字 → 映射為 profile/index?uid=123
// 實現見 handleSubdomain() 與 fallback() 中的數字判斷邏輯

4. 調試與效能監控

<?php
// 開啟調試
Rewrite::__debug(true);

// 獲取解析耗時
Rewrite::__parse();
echo "Route parse time: " . Rewrite::$timePass . " ms";
?>

六、與主流框架路由對比

特性自製 RewriteLaravelSymfonyThinkPHP 8
路由定義約定式 (檔案/類/方法)路由表 (routes/web.php) + 註解/屬性YAML/XML/PHP/屬性 + 路由集合路由表 + 註解/屬性
參數綁定手動 $_GET/$_POST自動依賴注入、模型綁定參數解析器、參數轉換器變數規則、類型轉換
中介軟體init() 鉤子手動調用Kernel + 中介軟體管道Event Listener / Middleware中介軟體管道
分組/前綴/域名目錄結構/子域名解析Route::group/domain/prefixRouteCollection / Host 匹配群組路由/域名路由
反向生成 URL手動拼接route() / url() helpergenerateUrl()url() / route()
快取路由無 (每請求解析)route:cache (生產必開)RouterCacheWarmer路由快取
學習曲線極低中高低
擴展性有限極強 (宏、自定義綁定)極強 (編譯器、擴展點)強 (擴展、行為)

七、遷移建議:何時換框架?

  • 繼續用自製:微型專案、極簡 API、學習原理、受限環境無 Composer、團隊熟悉度高
  • 遷移 Laravel:需生態套件、隊列/調度/事件/廣播、管理後台、快速構建、團隊標準化
  • 遷移 Symfony:企業級長期維護、複雜業務邏輯、高度解耦、微服務、強類型、測試驅動
  • 遷移 ThinkPHP:國內團隊、中文文檔友好、快速開發、兼容舊版遷移

八、核心代碼優化方向 (若堅持自製)

  1. 引入 FastRoute / League/Route:正則路由編譯、快取、參數提取、中介軟體管道
  2. PSR-7/15/17 相容:Request/Response 物件、Middleware 接口、Emitter
  3. 依賴注入容器:PHP-DI / Symfony DI / League/Container,解耦模組實例化
  4. 屬性路由 (PHP 8):#[Route('/article/{id}', methods: ['GET'])] 取代約定式
  5. OpenAPI/Swagger 自動生成:從屬性/註解生成文檔
  6. 單元測試覆蓋:路由解析、控制器行為、中介軟體邏輯

九、總結

自製路由器是理解框架核心機制的最佳途徑。本文實現展示了:

  1. 單入口 + 重寫:統一入口、靜態動態分離
  2. 約定優於配置:URL 映射類/方法,零配置新增模組
  3. 靜態方法調用:無實例化開銷、適合無狀態控制器
  4. 前置鉤子:init() 實現橫切關注點 (權限、佈局變數)
  5. 跨模組調用:__parseUrl() 打破模組隔離,服務複用
  6. 彈性兜底:子域名、短網址、404 統一處理

隨著專案增長,建議漸進式引入成熟路由組件,而非一次性重寫。核心原理通透,遷移自然水到渠成。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved