在一個web應用中攻臀,都有著一個從請求到響應的過程焕数,那么在這之間,服務器端可以怎樣進行一些相應的處理呢刨啸,今天借鑒YII2的源碼來仔細分析下這其中的一些細節(jié)堡赔。
請求方法
*如果存在 X_HTTP_METHOD_OVERRIDE HTTP 頭時,以該 HTTP 頭所指定的方法作為請求方法设联, 如 X-HTTP-Method-Override: PUT 表示該請求所要執(zhí)行的是 PUT方法善已;
如果 X_HTTP_METHOD_OVERRIDE 不存在,則以 REQUEST_METHOD 的值作為當前請求的方法离例。 如果連 REQUEST_METHOD 也不存在换团,則視該請求是一個 GET 請求艘包。
代碼實現(xiàn)如下:
if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
$requestmethod = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
} else {
$requestmethod = isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET';
}
這樣想虎,在請求方法成功的收集到了變量requestmethod中磷醋。
這里在特別介紹幾個函數(shù)邓线,可以用來判斷非HTTP協(xié)議所規(guī)定的請求類型:
function isAjax() {
return (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest');
}
function isPjax() {
return isAjax && !empty($_SERVER['HTTP_X_PJAX']);
}
function getFlash() {
return isset($_SERVER['HTTP_USER_AGENT']) && (stripos($_SERVER['HTTP_USER_AGENT']震庭, 'Shockwave') ! == false || stripos($_SERVER['HTTP_USER_AGENT'] !== false);
}
請求參數(shù)
在上面的代碼中器联,將所有的請求參數(shù)劃分為兩類拨拓, 一類是包含在 URL 中的渣磷,稱為查詢參數(shù)(Query Parameter)授瘦,或 GET 參數(shù)提完。 另一類是包含在請求體中的,需要根據(jù)請求體的內(nèi)容類型(Content Type)進行解析逐样,稱為 POST 參數(shù)官研。
我們來看一下YII2中是如何解析請求參數(shù)的:
function get($name = null, $defaultValue = null) {
if ($name = null) {
return getQueryParams();
} else {
return getQueryParams($name, $defaultValue);
}
}
function getQueryParams() {
return _GET;
}
function getQueryParam($name, $defaultValue = null) {
$params = getQueryParams();
return isset($params[$name]) ? $params[$name] : $defaultValue;
}
function post($name = null, $defaultValue = null) {
if ($name == null) {
return getBodyParams();
} else {
return getBodyParam($name, $defaultValue);
}
}
function getBodyParam($name, $defaultValue = null) {
$params = getBodyParams();
return isset($params[$name]) ? $params[$name] : $defaultValue;
}
function getBodyParams() {
$parsers = [
"*" => 'Parseall',
];
var_dump($_SERVER);
$contentType = getContentType();
if (($pos = strpos($contentType, ';')) !== false) {
$contentType = substr($contentType, 0, $pos);
}
if (isset($parsers[$contentType])) {
$parser = new $parsers[$contentType]();
if (!($parser instanceof RequestParser)) {
throw new Exception('wrong');
}
$_bodyParams = $parser->parse(getRawBody(), $contentType);
} elseif(isset($parsers['*'])) {
$parser = new $parsers['*']();
if (!($parser instanceof RequestParser)) {
throw new Exception('wrong');
}
$_bodyParams = $parser->parse(getRawBody(), $contentType);
} elseif ($requestmethod === 'POST') {
$_bodyParams = $_POST;
} else {
$_bodyParams = [];
mb_parse_str(getRawBody(), $_bodyParams);
}
return $_bodyParams;
}
function getContentType() {
if (isset($_SERVER['CONTENT_TYPE'])) {
return $_SERVER['CONTENT_TYPE'];
} elseif (isset($_SERVER['HTTP_CONTENT_TYPE'])) {
return $_SERVER['HTTP_CONTENT_TYPE'];
}
return null;
}
function getRawBody() {
return file_get_contents('php://input');
}
class RequestParser {
}
class Parseall extends RequestParser {
function parse($rawBody, $contentType = '') {
return $rawBody;
}
}
這是簡化后的邏輯代碼楼吃,這里而用到了只讀流php://input:
- php://input 是個只讀流孩锡,用于獲取請求體
- php://input 是返回整個 HTTP 請求中,除去 HTTP 頭部的全部原始內(nèi)容浇垦, 而不管是什么 Content Type(或稱為編碼方式)男韧。 相比較之下此虑, $_POST 只支持 application/x-www-form-urlencoded 和 multipart/form-data-encoded 兩種 ContentType口锭。其中前一種就是簡單的 HTML 表單以 method="post" 提交時的形式鹃操, 后一種主要是用于上傳文檔荆隘。因此,對于諸如 application/json 等 Content Type莫其,這往往是在 AJAX 場景下使用乱陡, 那么使用 $_POST 得到的是空的內(nèi)容憨颠,這時就必須使用php://input 积锅。
- 相比較于 $HTTP_RAW_POST_DATA 适篙, php://input 無需額外地在 php.ini 中 激活 always-populate-raw-post-data 箫爷,而且對于內(nèi)存的壓力也比較小
- 當編碼方式為 multipart/form-data-encoded 時, php://input 是無效的衩婚。這種情況一般為上傳文檔非春。 這種情況可以使用傳統(tǒng)的 $_FILES 奇昙。
請求頭
*同樣的我們以YII2的獲取請求頭信息的方法來進行分析 *
function getHeaders() {
if (function_exists('getallheaders')) {
$headers = getallheaders();
} elseif (function_exists('http_get_request_headers')) {
$headers = http_get_request_headers();
} else {
foreach($_SERVER as $name => $value) {
if (strncmp($name, 'HTTP_', 5) === 0) {
$name = str_replace(' ', '-',ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))));
$headers[$name] = $value;
}
}
}
return $headers;
}
這個方法根據(jù)不同的PHP環(huán)境敬矩,采用有效的方法來獲取請求頭部弧岳。
- getallheaders() 业踏,這個方法僅在將 PHP 作為 Apache 的一個模塊運行時有效勤家。
- http_get_request_headers() ,要求 PHP 啟用 HTTP 擴展热幔。
- $SERVER 數(shù)組的方法绎巨,需要遍歷整個數(shù)組蠕啄,并將所有以 HTTP* 元素加入到集合中去歼跟。 并且哈街,要將所有 HTTP_HEADER_NAME 轉(zhuǎn)換成 Header-Name 的形式。
框架的路由解析
*前面鋪墊了這么多撼港,干脆把YII2的路由解析也帖出來 帝牡,讓我們對此有一個更深層次的理解 *
YII2中可以定義相對應的路由規(guī)則靶溜,這里我們只介紹如何相應的邏輯
function init() {
//路由配置
$pattern = 'post/<action:\w+>/<id:\d+>';
//路由規(guī)則
$route = 'post/<action>';
//默認配置
$defaults = ['id' => 100];
$pattern = trim($pattern, '/');
$pattern = '/' . $pattern . '/';
$route = trim($route, '/');
if (strpos($route, '<') !== false && preg_match_all('/<(\w+)>/', $route, $matches)) {
foreach($matches[1] as $name) {
$_routeParams[$name] = "<$name>";
}
}
$tr = [
'.' => '\\.',
'*' => '\\*',
'$' => '\\$',
'[' => '\\[',
']' => '\\]',
'(' => '\\(',
'}' => '\\(',
];
$tr2 = [];
if (preg_match_all('/<(\w+):?([^>]+)?>/', $pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
foreach($matches as $match) {
$name = $match[1][0];
$patt = isset($match[2][0]) ? $match[2][0] : '[^\/]+';
if (array_key_exists($name, $defaults)) {
$length = strlen($match[0][0]);
$offset = $match[0][1];
if ($offset > 1 && $pattern[$offset - 1] === '/' && $pattern[$offset + $length] === '/') {
$tr["/<$name>"] = "(/(?P<$name>$patt))?";
} else {
$tr["<$name>"] = "(?P<$name>$patt)?";
}
} else {
$tr["<$name>"] = "(?P<$name>$patt)";
}
if (isset($_routeParams[$name])) {
$tr2["<$name>"] = "(?P<$name>$patt)";
} else {
$_paramRules[$name] = $patt === '[^\/]+' ? '' : "#^$patt$#u";
}
}
}
$_template = preg_replace('/<(\w+):?([^>]+)?>/', '<$1>', $pattern);
$pattern = '#^' . trim(strtr($_template, $tr), '/') . '$#u';
if (!empty($_routeParams)) {
$_routeRule = '#^' . strtr($route, $tr2) . '$#u';
}
return [$pattern, $_routeRule, $_routeParams, $defaults, $_paramRules, $_template];
}```
這個的功能其實就是根據(jù)參數(shù)生成標準的正則:#^post/(?P<action>\w+)(/(?P<id>\d+))?$#u
***為了方便个扰,源碼已經(jīng)被改成過程式代碼***
這里已經(jīng)到了路由解析递宅,所以干脆帶下YII2的路由構建思想 :
function createUrl($manager, $route, $params) {
$_route = 'post/<action>';
list($pattern, $_routeRule, $_routeParams, $defaults, $_paramRules, $_template) = init();
$tr = [];
if ($route !== $_route) {
if ($_routeRule !== null && preg_match($_routeRule, $route, $matches)) {
foreach($_routeParams as $name => $token) {
if (isset($defaults[$name]) && strcmp($defaults[$name], $matches[$name]) === 0) {
$tr[$token] = '';
} else {
$tr[$token] = $matches[$name];
}
}
} else {
return false;
}
}
foreach($defaults as $name => $value) {
if (isset($_routeParams[$name])) {
continue;
}
if(!isset($params[$name])) {
return false;
} elseif(strcmp($params[$name], $value) === 0) {
unset($params[$name]);
if (isset($_paramRules[$name])) {
$tr["<$name>"] = '';
}
} elseif (!isset($_paramRules[$name])) {
return false;
}
}
var_dump($_paramRules);
foreach( $_paramRules as $name => $rule) {
if(isset($params[$name]) && !is_array($params[$name]) && ($rule === '' || preg_match($rule, $params[$name]))) {
$tr["<$name>"] = $params[$name];
unset($params[$name]);
} elseif (!isset($defaults[$name]) ||isset($params[$name])) {
return false;
}
}
$url = trim(strtr($_template, $tr), '/');
echo $url;
}
其實整體的流程還是很好理解的,我們要先定義好規(guī)則俐填,如
['post/<action>:\w+><id:\d+> => 'post/<action>'],
默認id為100
這樣在init中的三個參數(shù):
$pattern = 'post/<action:\w+>/<id:\d+>';
$route = 'post/<action>';
$defaults = ['id' => 100];
經(jīng)過init函數(shù)后英融,會得到
[$pattern, $_routeRule, $_routeParams, $defaults, $_paramRules, $_template]六個參數(shù)驶悟,這些都是后面需要用到的。
![Paste_Image.png](http://upload-images.jianshu.io/upload_images/3079704-c1acdc070b7ec275.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
其實構造url就是判斷傳入的參數(shù)是否合乎規(guī)則给涕,是否有默認值够庙,如果 有的話傳入的參數(shù) 值是不是等于默認值,如果 是的話要對其進行省略操作耘眨。
構造url的反向當然就是解析URL了胆屿,這里我們也來把其改造成我們方便測試的方法
function parseUrl($manager, $request) {
$route = 'post/<action>';
extract(init());
$suffix = '.html';
$pathinfo = getpath();
$suffix = (string)($suffix === null ? '' : $suffix);
if ($suffix !== '' && $pathinfo !== '') {
$n = strlen($suffix);
if(substr_compare($pathinfo, $suffix, -$n , $n) === 0) {
$pathinfo = substr($pathinfo, 0, -$n);
if ($pathinfo === '') {
return false;
}
} else {
return false;
}
}
if (!preg_match($pattern, $pathinfo, $matches)) {
return false;
}
foreach($defaults as $name => $value) {
if (!isset($matches) || $matches[$name] === '') {
$matches[$name] = $value;
}
}
$params = $defaults;
$tr = [];
foreach($matches as $name => $value) {
if (isset($_routeParams[$name])) {
$tr[$_routeParams[$name]] = $value;
unset($params[$name]);
} elseif (isset($_paramRules[$name])) {
$params[$name] = $value;
}
}
if ($_routeRule !== null) {
$_route = strtr($route, $tr);
} else {
$_route = $route;
}
return [$_route, $params];
}
在parseurl中,先獲取pathinfo(*URL *中入口腳本之后纯趋、查詢參數(shù) *? *號之前的全部內(nèi)容纯命,即為 *PATH_INFO*),然后對其進行正則匹配亿汞,再查看 $routeParams,$_paramRules中有沒有相對的參數(shù) 疗我,構造出路由和參數(shù)