前面?[_請求(Reqeust)_](http://www.digpage.com/request.html#request)?部分我們講了用戶請求的基礎知識和命令行應用的Request,接下來繼續講Web應用的Request。
Web應用Request由?yii\web\Request?實現,這個類的代碼將近1400行,主要是一些功能的封裝罷了, 原理上沒有很復雜的東西。只是涉及到許多HTTP的有關知識,讀者朋友們可以自行查看相關的規范文檔, 如?[HTTP 1.1 協議](https://tools.ietf.org/html/rfc2616)?,?[CGI 1.1 規范](https://tools.ietf.org/html/rfc3875.html)?等。
同時,Yii大量引用了?$_SERVER?, 具體可以查看?[PHP文檔關于$_SERVER的內容](http://php.net/manual/en/reserved.variables.server.php)?, 此外,還涉及到PHP運行于不同的環境和模式下的一些細微差別。 這些內容比較細節,不影響大局,但是很影響理解,不過沒關系,我們在涉及到的時候,會點一點。
## 請求的方法[](http://www.digpage.com/web_request.html#id1 "Permalink to this headline")
根據?[HTTP 1.1 協議](https://tools.ietf.org/html/rfc2616)?,HTTP的請求可以有:GET, POST, PUT等8種方法 (Request Method)。除了用不到的 CONNECT 外,Yii支持全部的HTTP請求方法。
要獲取當前用戶請求的方法,可以使用?yii\web\Request::getMethod()
~~~
// 返回當前請求的方法,請留意方法名稱是大小寫敏感的,按規范應轉換為大寫字母
public function getMethod()
{
// $this->methodParam 默認值為 '_method'
// 如果指定 $_POST['_method'] ,表示使用POST請求來模擬其他方法的請求。
// 此時 $_POST['_method'] 即為所模擬的請求類型。
if (isset($_POST[$this->methodParam])) {
return strtoupper($_POST[$this->methodParam]);
// 或者使用 $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] 的值作為方法名。
} elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
return strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
// 或者使用 $_SERVER['REQUEST_METHOD'] 作為方法名,未指定時,默認為 GET 方法
} else {
return isset($_SERVER['REQUEST_METHOD']) ?
strtoupper($_SERVER['REQUEST_METHOD']) : 'GET';
}
}
~~~
這個方法使用了3種方法來獲取當前用戶的請求,優先級從高到低依次為:
* 當使用POST請求來模擬其他請求時,以?$_POST['_method']?作為當前請求的方法;
* 否則,如果存在?X_HTTP_METHOD_OVERRIDE?HTTP頭時,以該HTTP頭所指定的方法作為請求方法, 如?X-HTTP-Method-Override:?PUT?表示該請求所要執行的是 PUT 方法;
* 如果?X_HTTP_METHOD_OVERRIDE?不存在,則以?REQUEST_METHOD?的值作為當前請求的方法。 如果連REQUEST_METHOD?也不存在,則視該請求是一個 GET 請求。
前面兩種方法,主要是針對一些只支持GET和POST等有限方法的User Agent而設計的。
其中第一種方法是從Ruby on Rails中借鑒過來的, 通過在發送POST請求時,加入一個$_POST['_method']?的隱藏字段,來表示所要模擬的方法, 如PUT,DELETE等。這樣,就可以使得這些功能有限的User Agent也可以正常與服務器交互。 這種方法勝在簡便,隨手就來。
第二種方法則是使用?X_HTTP_METHOD_OVERRIDE?HTTP頭的辦法來指定所使用的請求類型。 這種方法勝在直接明了,約定俗成,更為規范、合理。
至于?REQUEST_METHOD?是?[CGI 1.1 規范](https://tools.ietf.org/html/rfc3875.html)?所定義的環境變量, 專門用來表明當前請求方法的。上面的代碼只是在未指定時默認為GET請求罷了。
當然,我們在開發過程中,其實并不怎么在乎當前的用戶請求是什么類型的請求,我們更在乎是不是某一類型的請求。 比如,對于同一個URL地址?http://api.digpage.com/post/123?, 如果是正常的GET請求,應該是查看編號為123的文章的意思。 但是如果是一個DELETE請求,則是表示刪除編號為123的文章的意思。我們在開發中,很可能就會這么寫:
~~~
if ($app->request->isDelete()){
$post->delete();
} else {
$post->view();
}
~~~
上面的代碼只是一個示意,與實際編碼是有一定出入的,主要看判斷分支的用法。 就是判斷請求是否是某一特定類型的請求。這些判斷在實際開發中,是很常用的。 于是Yii為我們封裝了許多方法專門用于執行這些判斷:
* getIsAjax()?是否是AJAX請求,這其實不是HTTP請求方法,但是實際使用上,這個是用得最多的。
* getIsDelete()?是否是DELETE請求
* getIsFlash()?是否是Adobe Flash 或 Adobe Flex 發出的請求,這其實也不是HTTP請求方法。
* getIsGet()?是否是一個GET請求
* getIsHead()?是否是一個HEAD請求
* getIsOptions()?是否是一個OPTIONS請求
* getIsPatch()?是否是PATCH請求
* getIsPjax()?是否是一個PJAX請求,這也并非是HTTP請求方法。
* getIsPost()?是否是一個POST請求
* getIsPut()?是否是一個PUT請求
上面10個方法請留意其中有3個并未是HTTP請求方法,主要是用于特定HTTP請求類型(AJAX、Flash、PJAX)的判斷。
除了這3個之外的其余7個方法,正好對應于HTTP 1.1 協議定義的7個方法。 而CONNECT方法由于Web開發在用不到,主要用于HTTP代理, 因此,Yii也就沒有為其設計一個所謂的?isConnect()?了,這是無用功。
上面的10個方法,再加一開始說的?getMehtod()?一共是11個方法,按照我們在?[_屬性(Property)_](http://www.digpage.com/property.html#property)?部分所說的, 這相當于定義了11個只讀屬性。我們以其中幾個為例,看看具體實現:
~~~
// 這個SO EASY,啥也不說了,Yii實現的7個HTTP方法都是這個路子。
public function getIsOptions()
{
// 注意在getMethod()時,輸出的是全部大寫的字符串
return $this->getMethod() === 'OPTIONS';
}
// AJAX請求是通過 X_REQUESTED_WITH 消息頭來判斷的
public function getIsAjax()
{
// 注意這里的XMLHttpRequest沒有全部大寫
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
$_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest';
}
// PJAX請求是AJAX請求的一種,增加了X_PJAX消息頭的定義
public function getIsPjax()
{
return $this->getIsAjax() && !empty($_SERVER['HTTP_X_PJAX']);
}
// HTTP_USER_AGENT消息頭中包含 'Shockwave' 或 'Flash' 字眼的(不區分大小寫),
// 就認為是FLASH請求
public function getIsFlash()
{
return isset($_SERVER['HTTP_USER_AGENT'])
&& (stripos($_SERVER['HTTP_USER_AGENT'], 'Shockwave') !== false
|| stripos($_SERVER['HTTP_USER_AGENT'], 'Flash') !== false);
}
~~~
上面提到的AJAX、PJAX、FLASH請求比較特殊,并非是HTTP協議所規定的請求類型,但是在實現中是會使用到的。 比如,對于一個請求,在非AJAX時,需要整個頁面返回給客戶端,而在AJAX請求時,只需要返回頁面片段即可。
這些特殊請求是通過特殊的消息頭實現的,具體的可以自行搜索相關的定義和規范。 至于那7個HTTP方法的判斷,擺明了是同一個路子,換瓶不換酒,?getMethod()?前人栽樹,他們后人乘涼。
## 請求的參數[](http://www.digpage.com/web_request.html#id4 "Permalink to this headline")
在實際開發中,開發者如果需要引用request,最常見的情況是為了獲取請求參數,以便作相應處理。 PHP有眾所周知的?$_GET?和?$_POST?等。相應地,Yii提供了一系列的方法用于獲取請求參數:
~~~
// 用于獲取GET參數,可以指定參數名和默認值
public function get($name = null, $defaultValue = null)
{
if ($name === null) {
return $this->getQueryParams();
} else {
return $this->getQueryParam($name, $defaultValue);
}
}
// 用于獲取所有的GET參數
// 所有的GET參數保存在 $_GET 或 $this->_queryParams 中。
public function getQueryParams()
{
if ($this->_queryParams === null) {
// 請留意這里并未使用 $this->_queryParams = $_GET 進行緩存。
// 說明一旦指定了 $_queryParams 則 $_GET 會失效。
return $_GET;
}
return $this->_queryParams;
}
// 根據參數名獲取單一的GET參數,不存在時,返回指定的默認值
public function getQueryParam($name, $defaultValue = null)
{
$params = $this->getQueryParams();
return isset($params[$name]) ? $params[$name] : $defaultValue;
}
// 類以于get(),用于獲取POST參數,也可以指定參數名和默認值
public function post($name = null, $defaultValue = null)
{
if ($name === null) {
return $this->getBodyParams();
} else {
return $this->getBodyParam($name, $defaultValue);
}
}
// 根據參數名獲取單一的POST參數,不存在時,返回指定的默認值
public function getBodyParam($name, $defaultValue = null)
{
$params = $this->getBodyParams();
return isset($params[$name]) ? $params[$name] : $defaultValue;
}
// 獲取所有POST參數,所有POST參數保存在 $this->_bodyParams 中
public function getBodyParams()
{
if ($this->_bodyParams === null) {
// 如果是使用 POST 請求模擬其他請求的
if (isset($_POST[$this->methodParam])) {
$this->_bodyParams = $_POST;
// 將 $_POST['_method'] 刪掉,剩余的$_POST就是了
unset($this->_bodyParams[$this->methodParam]);
return $this->_bodyParams;
}
// 獲取Content Type
// 對于 'application/json; charset=UTF-8',得到的是 'application/json'
$contentType = $this->getContentType();
if (($pos = strpos($contentType, ';')) !== false) {
$contentType = substr($contentType, 0, $pos);
}
// 根據Content Type 選擇相應的解析器對請求體進行解析
if (isset($this->parsers[$contentType])) {
// 創建解析器實例
$parser = Yii::createObject($this->parsers[$contentType]);
if (!($parser instanceof RequestParserInterface)) {
throw new InvalidConfigException(
"The '$contentType' request parser is invalid.
It must implement the yii\\web\\RequestParserInterface.");
}
// 將請求體解析到 $this->_bodyParams
$this->_bodyParams = $parser->parse($this->getRawBody(), $contentType);
// 如果沒有與Content Type對應的解析器,使用通用解析器
} elseif (isset($this->parsers['*'])) {
$parser = Yii::createObject($this->parsers['*']);
if (!($parser instanceof RequestParserInterface)) {
throw new InvalidConfigException(
"The fallback request parser is invalid.
It must implement the yii\\web\\RequestParserInterface.");
}
$this->_bodyParams = $parser->parse($this->getRawBody(),
$contentType);
// 連通用解析器也沒有
// 看看是不是POST請求,如果是,PHP已經將請求參數放到$_POST中了,直接用就OK了
} elseif ($this->getMethod() === 'POST') {
$this->_bodyParams = $_POST;
// 以上情況都不是,那就使用PHP的 mb_parse_str() 進行解析
} else {
$this->_bodyParams = [];
mb_parse_str($this->getRawBody(), $this->_bodyParams);
}
}
return $this->_bodyParams;
}
~~~
在上面的代碼中,將所有的請求參數劃分為兩類, 一類是包含在URL中的,稱為查詢參數(Query Parameter),或GET參數。 另一類是包含在請求體中的,需要根據請求體的內容類型(Content Type)進行解析,稱為POST參數。
其中,?get()?,?getQueryParams()?和?getQueryParam()?用于獲取查詢參數:
* get()?用于獲取GET參數,可以指定所要獲取的特定參數的參數名,在這個參數名不存在時,可以指定默認值。 當不指定參數名時,獲取所有的GET參數。 具體功能是由下面2個函數來實現的。
* getQueryParams()?用于獲取所有的GET參數。 這些參數的內容,保存在?$_GET?或$this->_queryParams?中。優先使用?$this->_queryParams?的。
* getQueryParam()?對應于?get()?用于獲取特定的GET參數的情況。
而?post()?,?getPostParams()?和?getPostParam()?用于獲取POST參數:
* post()?與?get()?類似,可以指定所要獲取的特定參數的參數名,在這個參數名不存在時,可以指定默認值。 當不指定參數名時,獲取所有的POST參數。 具體功能是由下面2個函數來實現的。
* getPostParam()?用于通過參數名獲取特定的POST參數,需要調用?getPostParams()?獲取所有的POST參數。
* getPostParams()?用于獲取所有的POST參數。
上面稍微復雜點的,可能就是?getPostParams()?了,我們就稍稍剖析下Yii是怎么解析POST參數的。 先講講這個方法所涉及到的一些東東:內容類型、請求解析器、請求體。
### 內容類型(Content-Type)[](http://www.digpage.com/web_request.html#content-type "Permalink to this headline")
在?getPostParams()?中,需要先獲取請求體的內容類型,然后采用相應的解析器對內容進行解析。
獲取內容類型,使用?getContentType()
~~~
public function getContentType()
{
if (isset($_SERVER["CONTENT_TYPE"])) {
return $_SERVER["CONTENT_TYPE"];
} elseif (isset($_SERVER["HTTP_CONTENT_TYPE"])) {
return $_SERVER["HTTP_CONTENT_TYPE"];
}
return null;
}
~~~
根據?[CGI 1.1 規范](https://tools.ietf.org/html/rfc3875.html)?, 內容類型由?CONTENT_TYPE?環境變量來表示。 而根據?[HTTP 1.1 協議](https://tools.ietf.org/html/rfc2616)?, 內容類型則是放在?CONTENT_TYPE?頭部中,然后由PHP賦值給?$_SERVER['HTTP_CONTENT_TYPE']?。 這里一般沒有沖突,因此發現哪個用哪個,就怕客戶端沒有給出(這種情況返回?null?)。
### 請求解析器[](http://www.digpage.com/web_request.html#id7 "Permalink to this headline")
在?getPostParams()?中,根據不同的Content Type 創建了相應的內容解析器對請求體進行解析。yii\web\Request?使用成員變量?public?$parsers?來保存一系列的解析器。 這個變量在配置時進行指定:
~~~
'request' => [
... ...
'parsers' => [
'application/json' => 'yii\web\JsonParser',
],
]
~~~
$parsers?是一個數組,數組的鍵是Content Type,如?applicaion/json?之類。 而數組的值則是對應于特定Content Type 的解析器,如?yii\web\JsonParser?。 這也是Yii實現的唯一一個現成的Parser,其他Content-Type,需要開發者自己寫了。
而且,可以以?*?為鍵指定一個解析器。那么該解析器將在一個Content Type找不到任何匹配的解析器后被使用。
yii\web\JsonParser?其實很簡單:
~~~
namespace yii\web;
use yii\base\InvalidParamException;
use yii\helpers\Json;
// 所有的解析器都要實現 RequestParserInterface
// 這個接口也只是要求實現 parse() 方法
class JsonParser implements RequestParserInterface
{
public $asArray = true;
public $throwException = true;
// 具體實現 parse()
public function parse($rawBody, $contentType)
{
try {
return Json::decode($rawBody, $this->asArray);
} catch (InvalidParamException $e) {
if ($this->throwException) {
throw new BadRequestHttpException(
'Invalid JSON data in request body: '
. $e->getMessage(), 0, $e);
}
return null;
}
}
}
~~~
這里使用?yii\helpers\Json::decode()?對請求體進行解析。這個?yii\helpers\Json?是個輔助類, 專門用于處理JSON格式數據。具體的內容我們這里就不做講解了,只需要了解這里可以將JSON格式數據解析出來就OK了, 學有余力的讀者朋友可以自己看看代碼。
### 請求體[](http://www.digpage.com/web_request.html#id8 "Permalink to this headline")
在?yii\web\Reqeust::getBodyParams()?和?yii\web\RequestParserInterface::parse()?中, 我們可以看到,需要將請求體傳入?parse()?進行解析,且請求體由?yii\web\Request::getRawBody()?可得。
yii\web\Request::getRawBody():
~~~
public function getRawBody()
{
if ($this->_rawBody === null) {
$this->_rawBody = file_get_contents('php://input');
}
return $this->_rawBody;
}
~~~
這個方法使用了?php://input?來獲取請求體,這個?php://input?有這么幾個特點:
* php://input?是個只讀流,用于獲取請求體。
* php://input?是返回整個HTTP請求中,除去HTTP頭部的全部原始內容, 而不管是什么Content Type(或稱為編碼方式)。 相比較之下,?$_POST?只支持?application/x-www-form-urlencoded?和multipart/form-data-encoded?兩種Content Type。其中前一種就是簡單的HTML表單以method="post"?提交時的形式, 后一種主要是用于上傳文檔。因此,對于諸如?application/json等Content Type,這往往是在AJAX場景下使用, 那么使用?$_POST?得到的是空的內容,這時就必須使用?php://input?。
* 相比較于?$HTTP_RAW_POST_DATA?,?php://input?無需額外地在php.ini中 激活always-populate-raw-post-data?,而且對于內存的壓力也比較小。
* 當編碼方式為?multipart/form-data-encoded?時,?php://input?是無效的。這種情況一般為上傳文檔。 這種情況可以使用傳統的?$_FILES?或者?yii\web\UploadedFile?。
## 請求的頭部[](http://www.digpage.com/web_request.html#id9 "Permalink to this headline")
yii\web\Request?使用一個成員變量?private?$_headers?來存儲請求頭。 而這個?$_header?其實是一個yii\web\HeaderCollection?,這是一個集合類的基本數據結構, 實現了SPL的?IteratorAggregate?,ArrayAccess?和?Countable?等接口。 因此,這個集合可以進行迭代、像數組一樣進行訪問、可被用于conut()?函數等。
這個數據結構相對簡單,我們就不展開占用篇幅了。我們要講的是怎么獲取請求的頭部。 這個是由yii\web\Request::getHeaders()?來實現的:
~~~
public function getHeaders()
{
if ($this->_headers === null) {
// 實例化為一個HeaderCollection
$this->_headers = new HeaderCollection;
// 使用 getallheaders() 獲取請求頭部,以數組形式返回
if (function_exists('getallheaders')) {
$headers = getallheaders();
// 使用 http_get_request_headers() 獲取請求頭部,以數組形式返回
} elseif (function_exists('http_get_request_headers')) {
$headers = http_get_request_headers();
// 使用 $_SERVER 數組獲取頭部
} else {
foreach ($_SERVER as $name => $value) {
// 針對所有 $_SERVER['HTTP_*'] 元素
if (strncmp($name, 'HTTP_', 5) === 0) {
// 將 HTTP_HEADER_NAME 轉換成 Header-Name 的形式
$name = str_replace(' ', '-',
ucwords(strtolower(str_replace('_', ' ',
substr($name, 5)))));
$this->_headers->add($name, $value);
}
}
return $this->_headers;
}
// 將數組形式的請求頭部變成集合的元素
foreach ($headers as $name => $value) {
$this->_headers->add($name, $value);
}
}
return $this->_headers;
}
~~~
這里用3種方法來嘗試獲取請求的頭部:
* getallheaders()?,這個方法僅在將PHP作為Apache的一個模塊運行時有效。
* http_get_request_headers()?,要求PHP啟用HTTP擴展。
* $_SERVER?數組的方法,需要遍歷整個數組,并將所有以?HTTP_*?元素加入到集合中去。 并且,要將所有?HTTP_HEADER_NAME?轉換成?Header-Name?的形式。
就是根據不同的PHP環境,采用有效的方法來獲取請求頭部,如此而已。
## 請求的解析[](http://www.digpage.com/web_request.html#id10 "Permalink to this headline")
我們前面就說過了,無論是命令行應用還是Web應用,他們的請求都要實現接口要求的?resolve()?, 以便明確這個用戶請求的路由和參數。下面就是?yii\web\Request::resolve()?的代碼:
~~~
public function resolve()
{
// 使用urlManager來解析請求
$result = Yii::$app->getUrlManager()->parseRequest($this);
if ($result !== false) {
list ($route, $params) = $result;
// 將解析出來的參數與 $_GET 參數進行合并
$_GET = array_merge($_GET, $params);
return [$route, $_GET];
} else {
throw new NotFoundHttpException(Yii::t('yii', 'Page not found.'));
}
}
~~~
看著很簡單吧?這才幾行,還沒有?getBodyParams()?的代碼多呢。
雖然簡單,但是有一個細節我們要留意,就是在解析出路由信息和參數的時候, 會把參數的內容加入到?$_GET?中去,這是合理的。
比如,對于?http://www.digpage.com/post/view/100?這個 100 在路由規則中,其實定義為一個 參數。其原始的形式應當是?http://www.digpage.com/index.php?r=post/view&id=100?。你說該 不該把?id?=?100重新寫回?$_GET?去?至于路由規則的內容,可以看看?[_路由(Route)_](http://www.digpage.com/route.html#route)?的內 容。
從這個?resolve()?是看不出來解析過程的復雜的,這個?yii\web\Request::resolve()?是個沒擔當的家伙,他把解析過程推給了 urlManager。 那我們就順藤摸瓜,一睹這個yii\web\UrlManager::parseRequest()?吧:
~~~
public function parseRequest($request)
{
// 啟用了 enablePrettyUrl 的情況
if ($this->enablePrettyUrl) {
// 獲取路徑信息
$pathInfo = $request->getPathInfo();
// 依次使用所有路由規則來解析當前請求
// 一旦有一個規則適用,后面的規則就沒有被調用的機會了
foreach ($this->rules as $rule) {
if (($result = $rule->parseRequest($this, $request)) !== false) {
return $result;
}
}
// 所有路由規則都不適用,又啟用了 enableStrictParsing ,
// 那只能返回 false 了。
if ($this->enableStrictParsing) {
return false;
}
// 所有路由規則都不適用,幸好還沒啟用 enableStrictParing,
// 那就用默認的解析邏輯
Yii::trace(
'No matching URL rules. Using default URL parsing logic.',
__METHOD__);
// 配置時所定義的fake suffix,諸如 ".html" 等
$suffix = (string) $this->suffix;
if ($suffix !== '' && $pathInfo !== '') {
// 這個分支的作用在于確保 $pathInfo 不能僅僅是包含一個 ".html"。
$n = strlen($this->suffix);
// 留意這個 -$n 的用法
if (substr_compare($pathInfo, $this->suffix, -$n, $n) === 0) {
$pathInfo = substr($pathInfo, 0, -$n);
// 僅包含 ".html" 的$pathInfo要之何用?掐死算了。
if ($pathInfo === '') {
return false;
}
// 后綴沒匹配上
} else {
return false;
}
}
return [$pathInfo, []];
// 沒有啟用 enablePrettyUrl的情況,那就更簡單了,
// 直接使用默認的解析邏輯就OK了
} else {
Yii::trace(
'Pretty URL not enabled. Using default URL parsing logic.',
__METHOD__);
$route = $request->getQueryParam($this->routeParam, '');
if (is_array($route)) {
$route = '';
}
return [(string) $route, []];
}
}
~~~
從上面代碼中可以看到,urlManager是按這么一個順序來解析用戶請求的:
* 先判斷是否啟用了 enablePrettyUrl,如果沒啟用,所有的路由和參數信息都在URL的查詢參數中, 很簡單就可以處理了。
* 通常都會啟用 enablePrettyUrl,由于路由和參數信息部分或全部變成了URL路徑。 經過了美化,使得URL看起來更友好,但化妝品總是比清水芙蓉要燒銀子,解析起來就有點費功夫了。
* 既然路由和參數信息變成了URL路徑,那么就先從URL路徑下手獲取路徑信息。
* 然后依次使用已經定義好的路由規則對當前請求進行解析,一旦有一個規則適用, 后續的路由規則就不會起作用了。
* 然后再對配置的?.html?等fake suffix進行處理。
這一過程中,有兩個重點,一個是獲取路徑信息,另一個就是使用路由規則對請求進行解析。下面我們依次進行講解。
### 獲取路徑信息[](http://www.digpage.com/web_request.html#id11 "Permalink to this headline")
在大多數情況下,我們還是會啟用 enablePrettyUrl 的,特別是在產品環境下。那么從上面的代碼來看,?yii\web\Request::getPathInfo()?的調用就不可避免。其實涉及到獲取路徑信息的方法有很多, 都在?yii\web\Request?中,這里暴露出來的,只是一個?getPathInfo()?,相關的方法有:
~~~
// 這個方法其實是調用 resolvePathInfo() 來獲取路徑信息的
public function getPathInfo()
{
if ($this->_pathInfo === null) {
$this->_pathInfo = $this->resolvePathInfo();
}
return $this->_pathInfo;
}
// 這個才是重點
protected function resolvePathInfo()
{
// 這個 getUrl() 調用的是 resolveRequestUri() 來獲取當前的URL
$pathInfo = $this->getUrl();
// 去除URL中的查詢參數部分,即 ? 及之后的內容
if (($pos = strpos($pathInfo, '?')) !== false) {
$pathInfo = substr($pathInfo, 0, $pos);
}
// 使用PHP urldecode() 進行解碼,所有 %## 轉成對應的字符, + 轉成空格
$pathInfo = urldecode($pathInfo);
// 這個正則列舉了各種編碼方式,通過排除這些編碼,來確認是 UTF-8 編碼
// 出處可參考 http://w3.org/International/questions/qa-forms-utf-8.html
if (!preg_match('%^(?:
[\x09\x0A\x0D\x20-\x7E] # ASCII
| [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte
| \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs
| [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
| \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates
| \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
| [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
| \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
)*$%xs', $pathInfo)
) {
$pathInfo = utf8_encode($pathInfo);
}
// 獲取當前腳本的URL
$scriptUrl = $this->getScriptUrl();
// 獲取Base URL
$baseUrl = $this->getBaseUrl();
if (strpos($pathInfo, $scriptUrl) === 0) {
$pathInfo = substr($pathInfo, strlen($scriptUrl));
} elseif ($baseUrl === '' || strpos($pathInfo, $baseUrl) === 0) {
$pathInfo = substr($pathInfo, strlen($baseUrl));
} elseif (isset($_SERVER['PHP_SELF']) && strpos($_SERVER['PHP_SELF'],
$scriptUrl) === 0) {
$pathInfo = substr($_SERVER['PHP_SELF'], strlen($scriptUrl));
} else {
throw new InvalidConfigException(
'Unable to determine the path info of the current request.');
}
// 去除 $pathInfo 前的 '/'
if ($pathInfo[0] === '/') {
$pathInfo = substr($pathInfo, 1);
}
return (string) $pathInfo;
}
~~~
從?resolvePathInfo()?來看,需要調用到的方法有?getUrl()?resolveRequestUri()?getScriptUrl()getBaseUrl()?等,這些都是與路徑信息密切相關的,讓我們分別都看一看。
#### Request URI[](http://www.digpage.com/web_request.html#request-uri "Permalink to this headline")
yii\web\Request::getUrl()?用于獲取Request URI的,實際上這只是一個屬性的封裝, 實質的代碼是在?yii\web\Request::resolveRequestUri()?中:
~~~
// 這個其實調用的是 resolveRequestUri() 來獲取當前URL
public function getUrl()
{
if ($this->_url === null) {
$this->_url = $this->resolveRequestUri();
}
return $this->_url;
}
// 這個方法用于獲取當前URL的URI部分,即主機或主機名之后的內容,包括查詢參數。
// 這個方法參考了 Zend Framework 1 的部分代碼,通過各種環境下的HTTP頭來獲取URI。
// 返回值為 $_SERVER['REQUEST_URI'] 或 $_SERVER['HTTP_X_REWRITE_URL'],
// 或 $_SERVER['ORIG_PATH_INFO'] + $_SERVER['QUERY_STRING']。
// 即,對于 http://www.digpage.com/index.html?helloworld,
// 得到URI為 index.html?helloworld
protected function resolveRequestUri()
{
// 使用了開啟了ISAPI_Rewrite的IIS
if (isset($_SERVER['HTTP_X_REWRITE_URL'])) {
$requestUri = $_SERVER['HTTP_X_REWRITE_URL'];
// 一般情況,需要去掉URL中的協議、主機、端口等內容
} elseif (isset($_SERVER['REQUEST_URI'])) {
$requestUri = $_SERVER['REQUEST_URI'];
// 如果URI不為空或以'/'打頭,則去除 http:// 或 https:// 直到第一個 /
if ($requestUri !== '' && $requestUri[0] !== '/') {
$requestUri = preg_replace('/^(http|https):\/\/[^\/]+/i',
'', $requestUri);
}
// IIS 5.0, PHP以CGI方式運行,需要把查詢參數接上
} elseif (isset($_SERVER['ORIG_PATH_INFO'])) {
$requestUri = $_SERVER['ORIG_PATH_INFO'];
if (!empty($_SERVER['QUERY_STRING'])) {
$requestUri .= '?' . $_SERVER['QUERY_STRING'];
}
} else {
throw new InvalidConfigException('Unable to determine the request URI.');
}
return $requestUri;
}
~~~
從上面的代碼我們可以知道,Yii針對不同的環境下,PHP的不同表現形式,通過一些分支判斷, 給出一個統一的路徑名或文件名。 辛辛苦苦那么多,其實就是為了消除不同環境對于開發的影響,使開發者可以更加專注于核心工作。
其實,作為一個開發框架,無論是哪種語言、用于哪個領域, 都需要為開發者提供在各種環境下的都表現一致的編程界面。 這也是開發者可以放心使用的基礎條件,如果在使用框架之后, 開發者仍需要考慮各種環境下會怎么樣怎么樣,那么這個框架注定短命。
這里有必要點一點涉及到的幾個?$_SERVER?變量。這里面提到的,讀者朋友可以自行閱讀?[PHP文檔關于$_SERVER的內容](http://php.net/manual/en/reserved.variables.server.php)?, 也可以看看?[CGI 1.1 規范的內容](https://tools.ietf.org/html/rfc3875.html)?。
REQUEST_URI
由HTTP 1.1 協議定義,指訪問某個頁面的URI,去除開頭的協議、主機、端口等信息。 如http://www.digpage.com:8080/index.php/foo/bar?queryParams?, REQUEST_URI為/index.php/foo/bar?queryParams?。
X-REWRITE-URL
當使用以開啟了ISAPI_Rewrite 的IIS作為服務器時,ISAPI_Rewrite會在未對原始URI作任何修改前, 將原始的 REQUEST_URI 以 X-REWRITE-URL HTTP頭保存起來。
PATH_INFO
CGI 1.1 規范所定義的環境變量。 從形式上看,http://www.digpage.com:8080/index.php?queryParams?。 它是整個URI中,在腳本標識之后、查詢參數???之前的部分。 對于Apache,需要設置?AcceptPathInfo?On?,且在一個URL沒有?部分的時候, PATH_INFO 無效。特殊的情況,如?http://www.digpage.com/index.php/, PATH_INFO 為?/?。 而對于Nginx,則需要設置:
~~~
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
fastcgi_param PATH_INFO $fastcgi_path_info;
~~~
ORIG_PATH_INFO
在PHP文檔中對它的解釋語焉不詳,“指未經PHP處理過的原始的PATH_INFO”。 這個在Apache和Nginx需要配置一番才行,但一般用不到,已經有PATH_INFO可以用了嘛。而在IIS中則有點怪, 對于http://www.digpage.com/index.php/?ORIG_PATH_INFO 為?/index.php/?; 對于http://www.digapge.com/index.php?ORIG_PATH_INFO 為?/index.php?。
根據上面這些背景知識,再來看?resolveRequestUri()?就簡單了:
* 最廣泛的情況,應當是使用 REQUEST_URI 來獲取。但是?resolveRequestUri()?卻先使用 X-REWRITE-URL, 這是為了防止REQUEST_URI被rewrite。
* 其次才是使用 REQUEST_URI,這對于絕大多數情況是完全夠用的了。
* 但REQUEST_URI畢竟只是規范的要求,Web服務器很有可能店大欺客、另立山頭,我們又不是第一次碰見了是吧? 所以,Yii使用了平時比較少用到的ORIG_PATH_INFO。
* 最后,按照規范要求進行規范化,該去頭的去頭,該續尾的續尾。去除主機信息段和查詢參數段后, 就大功告成了。
#### 入口腳本路徑[](http://www.digpage.com/web_request.html#id14 "Permalink to this headline")
yii\web\Request::getScriptUrl()?用于獲取入口腳本的相對路徑,也涉及到不同環境下PHP的不同表現。 我們還是先從代碼入手:
~~~
// 這個方法用于獲取當前入口腳本的相對路徑
public function getScriptUrl()
{
if ($this->_scriptUrl === null) {
// $this->getScriptFile() 用的是 $_SERVER['SCRIPT_FILENAME']
$scriptFile = $this->getScriptFile();
$scriptName = basename($scriptFile);
// 下面的這些判斷分支代碼,為各主流PHP framework所用,
// Yii, Zend, Symfony等都是大同小異。
if (basename($_SERVER['SCRIPT_NAME']) === $scriptName) {
$this->_scriptUrl = $_SERVER['SCRIPT_NAME'];
} elseif (basename($_SERVER['PHP_SELF']) === $scriptName) {
$this->_scriptUrl = $_SERVER['PHP_SELF'];
} elseif (isset($_SERVER['ORIG_SCRIPT_NAME']) &&
basename($_SERVER['ORIG_SCRIPT_NAME']) === $scriptName) {
$this->_scriptUrl = $_SERVER['ORIG_SCRIPT_NAME'];
} elseif (($pos = strpos($_SERVER['PHP_SELF'], '/' . $scriptName))
!== false) {
$this->_scriptUrl = substr($_SERVER['SCRIPT_NAME'], 0, $pos)
. '/' . $scriptName;
} elseif (!empty($_SERVER['DOCUMENT_ROOT'])
&& strpos($scriptFile, $_SERVER['DOCUMENT_ROOT']) === 0) {
$this->_scriptUrl = str_replace('\\', '/',
str_replace($_SERVER['DOCUMENT_ROOT'], '', $scriptFile));
} else {
throw new InvalidConfigException(
'Unable to determine the entry script URL.');
}
}
return $this->_scriptUrl;
}
~~~
上面的代碼涉及到了一些環境問題,點一點,大家了解下就OK了:
SCRIPT_FILENAME
當前腳本的實際物理路徑,比如?/var/www/digpage.com/frontend/web/index.php?, 或WIN平臺的D:\www\digpage.com\frontend\web\index.php?。 以Nginx為例,一般情況下,SCRIPT_FILENAME有以下配置項:
~~~
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
# 使用 document root 來得到物理路徑
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name
~~~
SCRIPT_NAME
CGI 1.1 規范所定義的環境變量,用于標識CGI腳本(而非腳本的輸出), 如http://www.digapge.com/path/index.php?中的?/path/index.php?。 仍以Nginx為例,SCRIPT_NAME一般情況下有?fastcgi_param?SCRIPT_NAME?$fastcgi_script_name?的設置。 絕大多數情況下,使用 SCRIPT_NAME 即可獲取當前腳本。
PHP_SELF
PHP_SELF 是PHP自己實現的一個?$_SERVER?變量,是相對于文檔根目錄(document root)而言的。 對于?http://www.digpage.com/path/index.php?queryParams?,PHP_SELF 為?/path/index.php?。 一般SCRIPT_NAME 與 PHP_SELF 無異。但是,在 PHP.INI 中,如?cgi.fix_pathinfo=1?(默認即為1)時, 對于形如?http://www.digpage.com/path/index.php/post/view/123?, 則PHP_SELF 為/path/index.php/post/view/123?。 而根據 CGI 1.1 規范,SCRIPT_NAME 僅為?/path/index.php?,至于剩余的?/post/view/123?則為PATH_INFO。
ORIG_SCRIPT_NAME
當PHP以CGI模式運行時,默認會對一些環境變量進行調整。 首當其沖的,就是 SCRIPT_NAME 的內容會變成?php.cgi?等二進制文件,而不再是CGI腳本文件。 當然,設置?cgi.fix_pathinfo=0?可以關閉這一默認行為。但這導致的副作用比較大,影響范圍過大,不宜使用。 但天無絕人之路,九死之地總留一線生機,那就是ORIG_SCRIPT_NAME,他保留了調整前 SCRIPT_NAME 的內容。 也就是說,在CGI模式下,可以使用 ORIG_SCRIPT_NAME 來獲取想要的SCRIPT_NAME。 請留意使用 ORIG_SCRIPT_NAME 前一定要先確認它是否存在。
交待完這些背景知識后,我們再來看看?yii\web\Request::getScriptUrl()?的邏輯:
* 先調用?yii\web\Request::getScriptFile()?, 通過?basename($_SERVER['SCRIPT_FILENAME'])?獲取腳本文件名。一般都是我們的入口腳本?index.php?。
* 絕大多數情況下,?base($_SERVER('SCRIPT_NAME'))?是與第一步獲取的?index.php?相同的。 如果這樣的話,則認為這個 SCRIPT_NAME 就是我們所要的腳本URL。
這也是規范的定義。但是既然稱為規范,說明并非是事實。 事實是由Web服務器來實現的,也就是說Web服務器可能進行修改。
另外,對于運行于CGI模式的PHP而言,使用 SCRIPT_NAME 也無法獲得腳本名。
* 那么我們轉而向PHP_SELF求助,這個是PHP內部實現的,不受Web服務器的影響。一般這個PHP_SELF也是可堪一用的, 但也不是放之四海而皆準,對于帶有PATH_INFO的URI,basename()?獲取的并不是腳本名。
* 于是我們再轉向 ORIG_SCRIPT_NAME 求助,如果PHP是運行于CGI模式,那么就可行。
* 再不成功,可能PHP并非運行于CGI模式(否則第4步就可以成功),且URI中帶有PATH_INFO(否則第二步就可以成功)。 對于這種情形,PHP_SELF的前面一截就是我們要的腳本URL 。
* 萬一以上情況都不符合,說明當前PHP運行的環境詭異莫測。 那只能寄希望于將 SCRIPT_FILENAME 中前面那截可能是Document Root的部分去掉,余下的作為腳本URL了。 前提是要有Document Root,且SCRIPT_FILENAME前面的部分可以匹配上。
#### Base Url[](http://www.digpage.com/web_request.html#base-url "Permalink to this headline")
獲取路徑信息的最后一個相關方法,就是?yii\web\Request::getBaseUrl():
~~~
// 獲取Base Url
public function getBaseUrl()
{
if ($this->_baseUrl === null) {
// 用上面的腳本路徑的父目錄,再去除末尾的 \ 和 /
$this->_baseUrl = rtrim(dirname($this->getScriptUrl()), '\\/');
}
return $this->_baseUrl;
}
~~~
這個Base Url很簡單,相信聰明如你肯定一目了然,我就不浪費篇幅了。
好了,上面就是?yii\web\Request::resolve()?中有關獲取路徑信息的內容。 下一步就是使用路由規則去解析當前請求了。
### 使用路由規則解析[](http://www.digpage.com/web_request.html#id15 "Permalink to this headline")
上面這么多有關從請求獲取路徑信息的內容,其實只完成了請求解析的第一步而已。 接下來,urlManager就要遍歷所有的路由規則來解析當前請求,直到有一個規則適用為止。
路由規則層面對于請求的解析,我們在?[_路由(Route)_](http://www.digpage.com/route.html#route)?的?[_解析URL_](http://www.digpage.com/route.html#parse-url)?部分已經講得很清楚了。
如果覺得《深入理解Yii2.0》對您有所幫助,也請[幫助《深入理解Yii2.0》](http://www.digpage.com/donate.html#donate)。 謝謝!
- 更新記錄
- 導讀
- Yii是什么
- Yii2.0的亮點
- 背景知識
- 如何閱讀本書
- Yii基礎
- 屬性(Property)
- 事件(Event)
- 行為(Behavior)
- Yii約定
- Yii應用的目錄結構和入口腳本
- 別名(Alias)
- Yii的類自動加載機制
- 環境和配置文件
- 配置項(Configuration)
- Yii模式
- MVC
- 依賴注入和依賴注入容器
- 服務定位器(Service Locator)
- 請求與響應(TBD)
- 路由(Route)
- Url管理
- 請求(Reqeust)
- Web應用Request
- Yii與數據庫(TBD)
- 數據類型
- 事務(Transaction)
- AcitveReocrd事件和關聯操作
- 樂觀鎖與悲觀鎖
- 《深入理解Yii2.0》視頻教程
- 第一講:基礎配置
- 第二講:用戶登錄
- 第三講:文章及評論的模型
- 附錄
- 附錄1:Yii2.0 對比 Yii1.1 的重大改進
- 附錄2:Yii的安裝
- 熱心讀者