深入理解Yii2
导读
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事件和关联操作
乐观锁与悲观锁
附录
附录1:Yii2.0 对比 Yii1.1 的重大改进
附录2:Yii的安装
本文档使用 MrDoc 发布
-
+
首页
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://worg/International/questions/qa-forms-utf-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 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?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。 * 最后,按照规范要求进行规范化,该去头的去头,该续尾的续尾。去除主机信息段和查询参数段后, 就大功告成了。 #### 入口脚本路径 > 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_ 部分已经讲得很清楚了。
admin
2022年12月19日 14:09
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码