Web应用Request

前面 请求(Reqeust) 部分我们讲了用户请求的基础知识和命令行应用的Request,接下来继续讲Web应用的Request。

Web应用Request由 yii\web\Request 实现,这个类的代码将近1400行,主要是一些功能的封装罢了, 原理上没有很复杂的东西。只是涉及到许多HTTP的有关知识,读者朋友们可以自行查看相关的规范文档, 如 HTTP 1.1 协议 , CGI 1.1 规范 等。

同时,Yii大量引用了 $_SERVER , 具体可以查看 PHP文档关于$_SERVER的内容 , 此外,还涉及到PHP运行于不同的环境和模式下的一些细微差别。 这些内容比较细节,不影响大局,但是很影响理解,不过没关系,我们在涉及到的时候,会点一点。

请求的方法

根据 HTTP 1.1 协议 ,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 规范 所定义的环境变量, 专门用来表明当前请求方法的。上面的代码只是在未指定时默认为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) 部分所说的, 这相当于定义了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() 前人栽树,他们后人乘凉。

请求的参数

在实际开发中,开发者如果需要引用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)
在 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 规范 , 内容类型由 CONTENT_TYPE 环境变量来表示。 而根据 HTTP 1.1 协议 , 内容类型则是放在 CONTENT_TYPE 头部中,然后由PHP赋值给 $_SERVER[‘HTTP_CONTENT_TYPE’] 。 这里一般没有冲突,因此发现哪个用哪个,就怕客户端没有给出(这种情况返回 null )。

请求解析器

在 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了, 学有余力的读者朋友可以自己看看代码。

请求体

在 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 。
请求的头部
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环境,采用有效的方法来获取请求头部,如此而已。

请求的解析

我们前面就说过了,无论是命令行应用还是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) 的内 容。

从这个 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进行处理。
这一过程中,有两个重点,一个是获取路径信息,另一个就是使用路由规则对请求进行解析。下面我们依次进行讲解。

获取路径信息

在大多数情况下,我们还是会启用 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

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的内容 , 也可以看看 CGI 1.1 规范的内容 。

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</PATH_INFO>?queryParams 。 它是整个URI中,在脚本标识之后、查询参数 ? 之前的部分。 对于Apache,需要设置 AcceptPathInfo On ,且在一个URL没有 </PATH_INFO> 部分的时候, 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。
最后,按照规范要求进行规范化,该去头的去头,该续尾的续尾。去除主机信息段和查询参数段后, 就大功告成了。
入口脚本路径
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

获取路径信息的最后一个相关方法,就是 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() 中有关获取路径信息的内容。 下一步就是使用路由规则去解析当前请求了。

使用路由规则解析

上面这么多有关从请求获取路径信息的内容,其实只完成了请求解析的第一步而已。 接下来,urlManager就要遍历所有的路由规则来解析当前请求,直到有一个规则适用为止。

路由规则层面对于请求的解析,我们在 路由(Route) 的 解析URL 部分已经讲得很清楚了。


http://www.taodudu.cc/news/show-5476645.html

相关文章:

  • 基线管理之Centos主机安全配置
  • SQL Server 数据库之SQL Server 数据库的安全设置
  • 防火墙安全配置
  • 计算机网络配置——交换机的安全配置
  • java怎么设置安全设置,win10系统打开java显示应用程序已安全设置被阻止的具体技巧...
  • RocketMq 安全配置
  • Centos、Windows主机安全配置
  • ssh(sshd)安全配置
  • SSH的安全配置
  • Centos安全配置
  • 安全配置
  • MySQL安全配置规范
  • 关于Vue分页循环动态获取页码
  • pdfjs 显示指定页码
  • 打印PDF 无法指定到 具体的页码
  • dataTables页码后面添加可输入页码跳转
  • LayUI table 刷新页面不重置页码
  • vue:分页页码组件
  • 自定义页码显示控件PageNumberView
  • pdf.js如何默认显示指定页码
  • datatable指定页码分页
  • jquery实现分页页码
  • html显示当前页码,使用pdf.js获取当前页码的笨办法
  • layui分页页码消失
  • 用In Design编辑页眉页脚和页码
  • 【提升栈】ListView优化
  • 银河麒麟V10配置rsync实现服务器同步备份
  • 服务器远程备份技巧,远程备份的实现
  • [转帖]服务器备份工具:Amanda,Bakula,Clonezilla,Rsnapshot,Mondo Rescue
  • 教程:Nodejs大漠插件开发游戏脚本实战(二)搭建项目

Web应用Request-请求与响应-(4.4)深入理解YII2.0相关推荐

  1. Java web—Servlet的请求与响应

    前言:         在家上网课快上疯了╰(‵□′)╯,上课连课本都没有,太难受了呜呜呜~, 只能在blog里记录一下这段时间的学习的内容,太难了- 一.servlet概述: 运行在WEB服务器端的 ...

  2. HTTP请求的响应头部Vary的理解

    1.引言 由于我主要是做Android开发的,所以Vary很陌生,今天看到OkHttp源码中,有对Vary的判断,就在网上查询并且仔细研究了一下,感觉比较有用,就记录一下. 2.讲解 简单说一下我对V ...

  3. CEF3:拦截http request请求和response响应(包括ajax请求和响应也能拦截到)

    文章目录 前言 思路 代码 前言 笔者在项目开发中有需求,需要拦截 js中 发起的 http 请求和响应数据 写到文件中,方便给开发人员或者测试人员查看.笔者拿到这个需求第一反应是,cef肯定有这种接 ...

  4. LAMP环境中如何重新部署一个Yii2.0 web项目

    使用Yii2.0 framework开发的项目,使用Github进行版本控制,现在要把这个项目部署到一个新的电脑/系统中: (1)安装LAMP (2)在/var/www/html目录下执行 git c ...

  5. ASP.NET Web API 记录请求响应数据到日志的一个方法

    原文:ASP.NET Web API 记录请求响应数据到日志的一个方法 原文:http://blog.bossma.cn/dotnet/asp-net-web-api-log-request-resp ...

  6. Scrapy - Request 和 Response(请求和响应)

    Requests and Responses:http://doc.scrapy.org/en/latest/topics/request-response.html Requests and Res ...

  7. Java Web(day05) —— 请求和响应

    一.Java web之请求和响应 Servlet最主要作用就是处理客户端请求并作出回应,为此,针对每次请求,Web容器在调用service()之前都会创建两个对象,分别是HttpServletRequ ...

  8. 【Java web】请求转发响应重定向

    文章目录 简介 请求转发 响应重定向 使用时机 简介 请求转发和响应重定向是Java web中两种资源跳转的方式.简单来说,对于完成一次请求需要许多特定的资源(如已经写好的页面或另一个Servlet) ...

  9. Python 爬虫 Request(请求头)和Response(响应头)的 内容详解 【爬虫资料二】

    Resquest请求头[以访问知乎]为例(使用Fiddler抓的包) 请求行包含的信息: 请求的方法(POST)  #其他的方法还有GET.HEAD.PUT.DELETE.OPTIONS.TRACE ...

最新文章

  1. 正确设置php-fpm和nginx防止网站被黑
  2. LeetCode 75. Sort Colors--Python解法
  3. 在一般处理程序(handler)中获取session的方法
  4. Linux命令netstat解读
  5. 利用反射自动封装成实体对象
  6. python基础十一之迭代器和生成器
  7. php pcre回溯攻击,PHP利用PCRE回溯次数限制绕过某些安全限制 | 码农网
  8. Java语言所有异常类均继承自_要继承自定义异常类的继承方式必须使用 ( ) 关键字_学小易找答案...
  9. c/c++教程 - 2.4.2.1~2 对象的初始化和清理,构造函数和析构函数,构造函数的分类和调用(有参构造,无参构造,普通构造,拷贝构造,括号法,显示法,隐式转换法,匿名对象)
  10. 机器学习——异常值检测
  11. 我的十年创作之路(三)——书稿创作经验谈
  12. st计算机编程语言,ST语法编程基础-ST语言简介
  13. ffmpeg中的时间单位以及时间转换函数(av_q2d av_rescale_q)
  14. 2022年01月最新 | 全国网络安全等级测评与检测评估机构目录,新增6家,共计224家...
  15. C语言结业作业,2019年本科课程-C语言程序设计结业试卷(附答案).doc
  16. Delphi下实现鼠标自动点击器
  17. 元祖字典 java_元祖和字典
  18. python一键抠图
  19. 一文排除WINDOWS-PYTHON3.7环境安装WORD2VEC包的所有坑
  20. 程序员修炼之道---之小工到专家

热门文章

  1. FIAA固定资产【10手工价值修正】
  2. 【GD32L233C-START】10、硬件SPI1驱动RC522
  3. 0xc0000142怎么修复(0xc0000142)
  4. python魔方程序算法_魔方机器人(一)还原算法
  5. shell脚本中写hive的sql语句
  6. Excel信息熵法求权
  7. 5G系统——网络选择
  8. JS 未结束的字符串常量
  9. 新奥新智面试(部分)
  10. lol无法启动此程序因为计算机丢失,win7系统玩英雄联盟lol提示计算机丢失auncher.dll的解决方法...