PHP內(nèi)置服務(wù)器實現(xiàn)URL重寫的實戰(zhàn)詳解
在PHP開發(fā)中,URL重寫是實現(xiàn)友好訪問路徑的核心手段(如將 /bazijp.html映射為 /?ac=bazijp),通常依賴Nginx/Apache的 rewrite指令。但本地開發(fā)時,PHP內(nèi)置服務(wù)器( php -S)更輕量高效,僅需通過自定義路由腳本即可實現(xiàn)等效重寫功能。本文將從基礎(chǔ)原理、環(huán)境配置、靜態(tài)資源兼容、復(fù)雜規(guī)則適配(如ThinkPHP,laravel項目)等維度,結(jié)合實際項目的重寫需求,提供一套可直接復(fù)用的解決方案,兼容PHP 5.6+主流版本。
一、核心原理:PHP內(nèi)置服務(wù)器的路由攔截機制
PHP內(nèi)置服務(wù)器本身不支持類似Nginx的rewrite語法,需通過路由腳本(如router.php)攔截所有請求并自定義轉(zhuǎn)發(fā)邏輯,核心流程如下:
- 客戶端發(fā)起請求(如
sm.tekin.cn/bazijp.html); - 內(nèi)置服務(wù)器將請求優(yōu)先轉(zhuǎn)發(fā)至路由腳本;
- 路由腳本根據(jù)預(yù)設(shè)規(guī)則重寫URL(如映射為
sm.tekin.cn/index.php?ac=bazijp); - 重寫后的請求轉(zhuǎn)發(fā)至項目統(tǒng)一入口(如
public/index.php); - 若請求為靜態(tài)資源(CSS/JS/圖片),則直接返回文件,不進入重寫流程。
關(guān)鍵前提:內(nèi)置服務(wù)器對命令行參數(shù)順序有嚴格要求,必須遵循php -S [地址:端口] -t [文檔根目錄] [路由腳本],參數(shù)錯位會導(dǎo)致路由失效或靜態(tài)資源無法加載。
二、基礎(chǔ)環(huán)境配置:從啟動到調(diào)試(適配主流項目結(jié)構(gòu))
2.1 標準項目目錄結(jié)構(gòu)
以常見的“Web根目錄與業(yè)務(wù)代碼分離”結(jié)構(gòu)為例(如ThinkPHP、Laravel等框架通用),目錄結(jié)構(gòu)如下:
project-root/ # 項目根目錄
├─ public/ # Web根目錄(線上 訪問根路徑)
│ ├─ statics/ # 靜態(tài)資源目錄(CSS/JS/圖片)
│ └─ index.php # 項目統(tǒng)一入口文件
└─ router.php # URL重寫路由腳本
2.2 啟動PHP內(nèi)置服務(wù)器
在項目根目錄執(zhí)行命令,指定public為Web根目錄,router.php為路由腳本,確保與線上環(huán)境目錄映射一致:
# 基礎(chǔ)啟動命令(本地訪問地址:http://127.0.0.1:8000) php -S 127.0.0.1:8000 -t public router.php # 啟用Xdebug調(diào)試(兼容PHP 5.6+) php -dxdebug.remote_enable=1 -dxdebug.remote_autostart=1 -S 127.0.0.1:8000 -t public router.php
2.3 VS Code調(diào)試配置(launch.json)
若需在IDE中調(diào)試業(yè)務(wù)邏輯,需配置launch.json確保調(diào)試與重寫協(xié)同生效,關(guān)鍵在于參數(shù)順序與線上一致:
{
"version": "0.2.0",
"configurations": [
{
"name": "PHP內(nèi)置服務(wù)器+URL重寫+調(diào)試",
"type": "php",
"runtimeExecutable": "/opt/local/bin/php56", // 本地PHP路徑(需與項目版本匹配)
"request": "launch",
"runtimeArgs": [
"-dxdebug.remote_enable=1",
"-dxdebug.remote_autostart=1",
"-dxdebug.remote_port=9000", // 與php.ini中Xdebug端口一致
"-S", "127.0.0.1:8000",
"-t", "public", // 匹配線上Web根目錄
"router.php" // 路由腳本必須放在最后
],
"cwd": "${workspaceRoot}",
"serverReadyAction": {
"pattern": "Development Server \\(http://localhost:([0-9]+)\\) started",
"uriFormat": "http://localhost:%s",
"action": "openExternally" // 啟動后自動打開本地調(diào)試地址
}
}
]
}
三、基礎(chǔ)路由腳本:解決靜態(tài)資源與簡單重寫需求
3.1 核心痛點:靜態(tài)資源404與規(guī)則失效
本地開發(fā)中,兩類問題最為常見:
- 靜態(tài)資源(如
/statics/ffsm/global.css)被路由腳本攔截,返回404; - 簡單重寫規(guī)則(如
/hehun.html→/?ac=hehun)不生效,無法訪問目標模塊。
3.2 基礎(chǔ)版路由腳本(兼容PHP 5.6)
核心邏輯:優(yōu)先處理靜態(tài)資源,再執(zhí)行URL重寫,確保靜態(tài)資源加載與動態(tài)路由分離:
<?php
// 項目根目錄與Web根目錄定義
$projectRoot = __DIR__;
$webRoot = rtrim($projectRoot . '/public', '/') . '/';
// 1. 解析并標準化請求URI
$requestUri = $_SERVER['REQUEST_URI'];
$uriParts = parse_url($requestUri);
$uriPath = isset($uriParts['path']) ? $uriParts['path'] : '/';
$uriPath = preg_replace('#/\.\./#', '/', $uriPath); // 過濾目錄遍歷攻擊
$uriPath = rtrim($uriPath, '/'); // 統(tǒng)一去除末尾斜杠(如`/suanming/scbz/`→`/suanming/scbz`)
$uriPath = $uriPath === '' ? '/' : $uriPath;
// 2. 優(yōu)先處理靜態(tài)資源(存在則直接返回)
$requestedFile = $webRoot . ltrim($uriPath, '/');
if (file_exists($requestedFile) && is_file($requestedFile) && !is_dir($requestedFile)) {
// 設(shè)置正確MIME類型,避免瀏覽器解析異常
$mimeType = getMimeType($requestedFile);
if ($mimeType) header("Content-Type: {$mimeType}");
readfile($requestedFile);
exit;
}
// 3. 定義基礎(chǔ)URL重寫規(guī)則(可根據(jù)項目需求擴展)
$rewriteRules = [
'#^/index\.html$#' => '/index.php', // 首頁規(guī)則
'#^/bazijp\.html$#' => '/?ac=bazijp', // 八字精批模塊規(guī)則
'#^/hehun\.html$#' => '/?ac=hehun', // 合婚模塊規(guī)則
'#^/aboutus\.html$#' => '/?ac=aboutus', // 關(guān)于我們頁面規(guī)則
'#^/xyd-([0-9]+)\.html$#' => '/?ac=xyd&id=$1', // 詳情頁動態(tài)規(guī)則
'#^/([^/]+)\.html$#' => '/index.php?ac=$1', // 最后執(zhí)行:通用.html規(guī)則(最寬泛,避免覆蓋前面的具體規(guī)則)
];
// 4. 應(yīng)用重寫規(guī)則
$newUri = $uriPath;
foreach ($rewriteRules as $pattern => $target) {
if (preg_match($pattern, $uriPath)) {
$newUri = preg_replace($pattern, $target, $uriPath);
break; // 匹配到即終止,避免規(guī)則沖突
}
}
// 5. 處理查詢參數(shù)(保留原參數(shù),新規(guī)則參數(shù)覆蓋同名原參數(shù))
$originalQuery = isset($uriParts['query']) ? $uriParts['query'] : '';
$newUriParts = parse_url($newUri);
$newPath = isset($newUriParts['path']) ? $newUriParts['path'] : '/';
$newQuery = isset($newUriParts['query']) ? $newUriParts['query'] : '';
$finalQuery = '';
if (!empty($originalQuery) && !empty($newQuery)) {
parse_str($originalQuery, $originalParams);
parse_str($newQuery, $newParams);
$mergedParams = array_merge($originalParams, $newParams);
$finalQuery = http_build_query($mergedParams);
} elseif (!empty($originalQuery)) {
$finalQuery = $originalQuery;
} else {
$finalQuery = $newQuery;
}
// 6. 更新服務(wù)器變量,轉(zhuǎn)發(fā)到統(tǒng)一入口
$finalUri = $newPath . ($finalQuery ? "?{$finalQuery}" : '');
$_SERVER['REQUEST_URI'] = $finalUri;
$_SERVER['SCRIPT_NAME'] = '/index.php';
$_SERVER['QUERY_STRING'] = $finalQuery;
parse_str($finalQuery, $_GET); // 同步更新GET參數(shù),適配框架參數(shù)獲取
// 7. 執(zhí)行入口文件
$indexFile = $webRoot . 'index.php';
if (file_exists($indexFile)) {
include_once $indexFile;
} else {
http_response_code(404);
echo "404 Not Found:public/index.php 入口文件不存在";
}
exit;
/**
* 兼容PHP 5.6的MIME類型獲取
* @param string $file 文件路徑
* @return string|null MIME類型
*/
function getMimeType($file) {
$extension = strtolower(pathinfo($file, PATHINFO_EXTENSION));
$mimeMap = [
'css' => 'text/css', 'js' => 'application/javascript',
'html' => 'text/html', 'jpg' => 'image/jpeg',
'png' => 'image/png', 'gif' => 'image/gif', 'ico' => 'image/x-icon'
];
return isset($mimeMap[$extension]) ? $mimeMap[$extension] : null;
}
3.3 基礎(chǔ)規(guī)則測試
- 訪問
http://127.0.0.1:8000/bazijp.html,通過var_dump($_GET)應(yīng)看到array('ac' => 'bazijp'); - 訪問
http://127.0.0.1:8000/xyd-123.html,應(yīng)看到array('ac' => 'xyd', 'id' => '123'); - 訪問
http://127.0.0.1:8000/statics/ffsm/global.css,應(yīng)直接返回CSS文件內(nèi)容。
四、進階:適配復(fù)雜重寫規(guī)則(以ThinkPHP項目為例)
實際項目中,常需處理大量復(fù)雜重寫規(guī)則(如多模塊路由、動態(tài)參數(shù)拼接)。例如某命理類項目的Nginx規(guī)則片段:
rewrite ^/aboutus.html /index.php?ac=aboutus last; rewrite ^/xyd-([0-9]+).html /index.php?ac=xyd&id=$1 last; rewrite ^/(.*).html /index.php?ac=$1 last; rewrite ^/show-([0-9]+).html /index.php?ct=news&ac=show&id=$1;
這類規(guī)則遷移時,核心挑戰(zhàn)是避免通用規(guī)則覆蓋具體規(guī)則(如/aboutus.html不能被/.+\.html的通用規(guī)則錯誤映射)。
4.1 復(fù)雜規(guī)則適配方案:規(guī)則分組排序
核心思路:將規(guī)則按“精準度”分組,精準規(guī)則優(yōu)先匹配,通用規(guī)則兜底,確保每個模塊的路由邏輯與線上一致。
完整路由腳本(適配復(fù)雜項目規(guī)則)
<?php
$projectRoot = __DIR__;
$webRoot = rtrim($projectRoot . '/public', '/') . '/';
// 1. 標準化請求URI
$requestUri = $_SERVER['REQUEST_URI'];
$uriParts = parse_url($requestUri);
$uriPath = isset($uriParts['path']) ? $uriParts['path'] : '/';
$uriPath = preg_replace('#/\.\./#', '/', $uriPath);
$uriPath = rtrim($uriPath, '/');
$uriPath = $uriPath === '' ? '/' : $uriPath;
// 2. 優(yōu)先處理靜態(tài)資源
$requestedFile = $webRoot . ltrim($uriPath, '/');
if (file_exists($requestedFile) && is_file($requestedFile) && !is_dir($requestedFile)) {
$mimeType = getMimeType($requestedFile);
if ($mimeType) header("Content-Type: {$mimeType}");
readfile($requestedFile);
exit;
}
// 3. 規(guī)則分組:精準規(guī)則 → 通用規(guī)則(避免寬覆蓋窄)
// 注意下面的規(guī)則需要根據(jù)你自己的項目進行修改,這里僅做示例 更多可以參考 https://sm.tekin.cn 站點的URL重寫
// --------------------------
// 第一組:精準規(guī)則(無動態(tài)參數(shù),固定URL)
// --------------------------
$exactRules = [
// 基礎(chǔ)頁面
'#^/index\.html$#' => '/index.php',
'#^/aboutus\.html$#' => '/index.php?ac=aboutus',
'#^/contactus\.html$#' => '/index.php?ac=contactus',
];
// --------------------------
// 第二組:通用規(guī)則(帶動態(tài)參數(shù)、后綴匹配)
// --------------------------
$generalRules = [
// 帶ID的精準后綴規(guī)則
'#^/xyd-([0-9]+)\.html$#' => '/index.php?ac=xyd&id=$1',
// 姓名模塊動態(tài)路徑
'#^/xmfx/([^/]+)$#' => '/index.php?ct=xingming&ac=xmfx&name=$1',
'#^/xqlist-([0-9]+)-([0-9]+)-([0-9]+)-([0-9]+)\.html$#' => '/index.php?ct=xq&xid=$1&sex=$2&hs=$3&page=$4',
// 最后執(zhí)行:通用.html規(guī)則(兜底)
'#^/([^/]+)\.html$#' => '/index.php?ac=$1',
];
// 4. 執(zhí)行重寫:先精準后通用
$newUri = $uriPath;
$ruleMatched = false;
// 第一步:匹配精準規(guī)則(核心業(yè)務(wù)優(yōu)先)
foreach ($exactRules as $pattern => $target) {
if (preg_match($pattern, $uriPath)) {
$newUri = preg_replace($pattern, $target, $uriPath);
$ruleMatched = true;
break;
}
}
// 第二步:精準規(guī)則未匹配時,匹配通用規(guī)則
if (!$ruleMatched) {
foreach ($generalRules as $pattern => $target) {
if (preg_match($pattern, $uriPath)) {
$newUri = preg_replace($pattern, $target, $uriPath);
break;
}
}
}
// 5. 合并查詢參數(shù)(同基礎(chǔ)版邏輯)
$originalQuery = isset($uriParts['query']) ? $uriParts['query'] : '';
$newUriParts = parse_url($newUri);
$newPath = isset($newUriParts['path']) ? $newUriParts['path'] : '/';
$newQuery = isset($newUriParts['query']) ? $newUriParts['query'] : '';
$finalQuery = '';
if (!empty($originalQuery) && !empty($newQuery)) {
parse_str($originalQuery, $originalParams);
parse_str($newQuery, $newParams);
$mergedParams = array_merge($originalParams, $newParams);
$finalQuery = http_build_query($mergedParams);
} elseif (!empty($originalQuery)) {
$finalQuery = $originalQuery;
} else {
$finalQuery = $newQuery;
}
// 6. 轉(zhuǎn)發(fā)到入口文件
$finalUri = $newPath . ($finalQuery ? "?{$finalQuery}" : '');
$_SERVER['REQUEST_URI'] = $finalUri;
$_SERVER['SCRIPT_NAME'] = '/index.php';
$_SERVER['QUERY_STRING'] = $finalQuery;
parse_str($finalQuery, $_GET);
$indexFile = $webRoot . 'index.php';
if (file_exists($indexFile)) {
include_once $indexFile;
} else {
http_response_code(404);
echo "404 Not Found:public/index.php 入口文件不存在";
}
exit;
// MIME類型函數(shù)(同基礎(chǔ)版)
function getMimeType($file) {
$extension = strtolower(pathinfo($file, PATHINFO_EXTENSION));
$mimeMap = [
'css' => 'text/css', 'js' => 'application/javascript',
'html' => 'text/html', 'jpg' => 'image/jpeg',
'png' => 'image/png', 'gif' => 'image/gif', 'ico' => 'image/x-icon'
];
return isset($mimeMap[$extension]) ? $mimeMap[$extension] : null;
}
?>
4.2 規(guī)則適配關(guān)鍵點
- 分組邏輯:
$exactRules存放固定URL(如/aboutus.html),$generalRules存放動態(tài)URL(如/([^/]+)\.html),確保精準規(guī)則不被覆蓋; - 通用規(guī)則內(nèi)部順序:即使在
$generalRules中,也需從“具體”到“寬泛”排序(如先匹配/xyd-([0-9]+)\.html,再匹配/([^/]+)\.html); - 參數(shù)兼容性:保留原請求中的查詢參數(shù)(如
/user/abc?foo=bar→/index.php?ct=user&ac=abc&foo=bar),符合線上重寫習(xí)慣。
五、常見問題與解決方案
5.1 靜態(tài)資源404
原因:路由腳本未優(yōu)先處理靜態(tài)資源,或$webRoot路徑拼接錯誤(如多寫斜杠);
解決方案:確保靜態(tài)資源判斷邏輯在重寫規(guī)則之前,$requestedFile路徑格式為public/statics/ffsm/global.css。
5.2 重寫規(guī)則不生效
原因:內(nèi)置服務(wù)器參數(shù)順序錯誤(如router.php放在-t public之前),或正則表達式錯誤(如.未轉(zhuǎn)義);
解決方案:嚴格遵循php -S 地址 -t 根目錄 路由腳本,正則使用#^/aboutus\.html$#格式。
5.3 PHP 5.6兼容性問題
問題:使用??空合并運算符導(dǎo)致語法錯誤;
解決方案:用isset()+三元運算符替代(如$uriPath = isset($uriParts['path']) ? $uriParts['path'] : '/')。
六、總結(jié)
PHP內(nèi)置服務(wù)器的URL重寫核心在于路由腳本的設(shè)計,通過“靜態(tài)資源優(yōu)先+規(guī)則分組排序”的思路,可實現(xiàn)與Nginx/Apache等效的重寫功能。關(guān)鍵要點如下:
- 參數(shù)順序:啟動時嚴格遵循
-S 地址 -t 根目錄 路由腳本; - 靜態(tài)優(yōu)先:避免靜態(tài)資源進入重寫流程;
- 規(guī)則分組:按“精準→通用”排序,解決規(guī)則覆蓋問題;
- 版本兼容:針對PHP 5.6等舊版本調(diào)整語法,確保項目可用性。
這套方案可直接復(fù)用于ThinkPHP等主流框架的本地開發(fā),無需依賴重型Web服務(wù)器,有效提升開發(fā)效率,減少環(huán)境差異導(dǎo)致的問題。
到此這篇關(guān)于 PHP內(nèi)置服務(wù)器實現(xiàn)URL重寫的實戰(zhàn)詳解的文章就介紹到這了,更多相關(guān) PHP URL重寫內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
LINUX下PHP程序?qū)崿F(xiàn)WORD文件轉(zhuǎn)化為PDF文件的方法
這篇文章主要介紹了LINUX下PHP程序?qū)崿F(xiàn)WORD文件轉(zhuǎn)化為PDF文件的方法,涉及php針對Word文檔與pdf格式文件的相關(guān)操作技巧,需要的朋友可以參考下2016-05-05
深思 PHP 數(shù)組遍歷的差異(array_diff 的實現(xiàn))
還是部門無聊的考題,不過這次考的是 PHP 的能力。題目如下: 給你兩個分別有 5000 個元素的數(shù)組,計算他們的差集 -- 說白了也就是用 PHP 和你認為最好的算法實現(xiàn) array_diff 的算法。初次接到這個題目,我發(fā)現(xiàn)這非常的簡單,于是按照以往的經(jīng)驗“隨便”寫了一個:2008-03-03
php數(shù)組相加 array(“a”)+array(“b”)結(jié)果還是array(“a”)
同一個數(shù)組里面如果有相同的鍵名,則前面一個鍵名的值將會被覆蓋(overwritten)2012-09-09
采用header定義為文件然后readfile下載(隱藏下載地址)
有時候我們?yōu)榱穗[藏真實的下載地址,我們通過采用header定義為文件然后readfile下載,但這樣會加大服務(wù)器的負擔(dān),一般不建議下載量比較大的文件2014-01-01

