php请求分析与处理

在一个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中有没有相对的参数 ,构造出路由和参数
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容