在一个web应用中,都有着一个从请求到响应的过程,那么在这之间,服务器端可以怎样进行一些相应的处理呢,今天借鉴YII2的源码来仔细分析下这其中的一些细节。
请求方法
*如果存在 X_HTTP_METHOD_OVERRIDE HTTP 头时,以该 HTTP 头所指定的方法作为请求方法, 如 X-HTTP-Method-Override: PUT 表示该请求所要执行的是 PUT方法;
如果 X_HTTP_METHOD_OVERRIDE 不存在,则以 REQUEST_METHOD 的值作为当前请求的方法。 如果连 REQUEST_METHOD 也不存在,则视该请求是一个 GET 请求。
代码实现如下:
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中。
这里在特别介绍几个函数,可以用来判断非HTTP协议所规定的请求类型:
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);
}
请求参数
在上面的代码中,将所有的请求参数划分为两类, 一类是包含在 URL 中的,称为查询参数(Query Parameter),或 GET 参数。 另一类是包含在请求体中的,需要根据请求体的内容类型(Content Type)进行解析,称为 POST 参数。
我们来看一下YII2中是如何解析请求参数的:
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 头部的全部原始内容, 而不管是什么 Content Type(或称为编码方式)。 相比较之下, $_POST 只支持 application/x-www-form-urlencoded 和 multipart/form-data-encoded 两种 ContentType。其中前一种就是简单的 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 。
请求头
*同样的我们以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;
}
这个方法根据不同的PHP环境,采用有效的方法来获取请求头部。
- getallheaders() ,这个方法仅在将 PHP 作为 Apache 的一个模块运行时有效。
- http_get_request_headers() ,要求 PHP 启用 HTTP 扩展。
- $SERVER 数组的方法,需要遍历整个数组,并将所有以 HTTP* 元素加入到集合中去。 并且,要将所有 HTTP_HEADER_NAME 转换成 Header-Name 的形式。
框架的路由解析
*前面铺垫了这么多,干脆把YII2的路由解析也帖出来 ,让我们对此有一个更深层次的理解 *
YII2中可以定义相对应的路由规则,这里我们只介绍如何相应的逻辑
function init() {
//路由配置
$pattern = 'post/<action:\w+>/<id:\d+>';
//路由规则
$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];
}```
这个的功能其实就是根据参数生成标准的正则:#^post/(?P<action>\w+)(/(?P<id>\d+))?$#u
***为了方便,源码已经被改成过程式代码***
这里已经到了路由解析,所以干脆带下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;
}
其实整体的流程还是很好理解的,我们要先定义好规则,如
['post/<action>:\w+><id:\d+> => 'post/<action>'],
默认id为100
这样在init中的三个参数:
$pattern = 'post/<action:\w+>/<id:\d+>';
$route = 'post/<action>';
$defaults = ['id' => 100];
经过init函数后,会得到
[$pattern, $_routeRule, $_routeParams, $defaults, $_paramRules, $_template]六个参数,这些都是后面需要用到的。
![Paste_Image.png](http://upload-images.jianshu.io/upload_images/3079704-c1acdc070b7ec275.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
其实构造url就是判断传入的参数是否合乎规则,是否有默认值,如果 有的话传入的参数 值是不是等于默认值,如果 是的话要对其进行省略操作。
构造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 *中入口脚本之后、查询参数 *? *号之前的全部内容,即为 *PATH_INFO*),然后对其进行正则匹配,再查看 $routeParams,$_paramRules中有没有相对的参数 ,构造出路由和参数