解读PHP的Yii框架中请求与响应的处理流程
一、请求(Requests)
请求:
一个应用的请求是用yii\web\Request对象来表示的,该对象提供了诸如请求参数(译者注:通常是GET参数或者POST参数)、HTTP头、cookies等信息。默认情况下,对于一个给定的请求,你可以通过requestapplicationcomponent应用组件(yii\web\Request类的实例)获得访问相应的请求对象。在本章节,我们将介绍怎样在你的应用中使用这个组件。
1.请求参数
要获取请求参数,你可以调用request组件的yii\web\Request::get()方法和yii\web\Request::post()方法。他们分别返回$_GET和$_POST的值。例如,
$request=Yii::$app->request; $get=$request->get(); //等价于:$get=$_GET; $id=$request->get('id'); //等价于:$id=isset($_GET['id'])?$_GET['id']:null; $id=$request->get('id',1); //等价于:$id=isset($_GET['id'])?$_GET['id']:1; $post=$request->post(); //等价于:$post=$_POST; $name=$request->post('name'); //等价于:$name=isset($_POST['name'])?$_POST['name']:null; $name=$request->post('name',''); //等价于:$name=isset($_POST['name'])?$_POST['name']:'';
信息:建议你像上面那样通过request组件来获取请求参数,而不是直接访问$_GET和$_POST。这使你更容易编写测试用例,因为你可以伪造数据来创建一个模拟请求组件。
当实现RESTfulAPIs接口的时候,你经常需要获取通过PUT,PATCH或者其他的requestmethods请求方法提交上来的参数。你可以通过调用yii\web\Request::getBodyParam()方法来获取这些参数。例如,
$request=Yii::$app->request; //返回所有参数 $params=$request->bodyParams; //返回参数"id" $param=$request->getBodyParam('id');
信息:不同于GET参数,POST,PUT,PATCH等等这些提交上来的参数是在请求体中被发送的。当你通过上面介绍的方法访问这些参数的时候,request组件会解析这些参数。你可以通过配置yii\web\Request::parsers属性来自定义怎样解析这些参数。
2.请求方法
你可以通过Yii::$app->request->method表达式来获取当前请求使用的HTTP方法。这里还提供了一整套布尔属性用于检测当前请求是某种类型。例如,
$request=Yii::$app->request; if($request->isAjax){/*该请求是一个AJAX请求*/} if($request->isGet){/*请求方法是GET*/} if($request->isPost){/*请求方法是POST*/} if($request->isPut){/*请求方法是PUT*/}
3.请求URLs
request组件提供了许多方式来检测当前请求的URL。
假设被请求的URL是http://example.com/admin/index.php/product?id=100,你可以像下面描述的那样获取URL的各个部分:
- yii\web\Request::url:返回/admin/index.php/product?id=100,此URL不包括hostinfo部分。
- yii\web\Request::absoluteUrl:返回http://example.com/admin/index.php/product?id=100,包含hostinfode的整个URL。
- yii\web\Request::hostInfo:返回http://example.com,只有hostinfo部分。
- yii\web\Request::pathInfo:返回/product,这个是入口脚本之后,问号之前(查询字符串)的部分。
- yii\web\Request::queryString:返回id=100,问号之后的部分。
- yii\web\Request::baseUrl:返回/admin,hostinfo之后,入口脚本之前的部分。
- yii\web\Request::scriptUrl:返回/admin/index.php,没有pathinfo和查询字符串部分。
- yii\web\Request::serverName:返回example.com,URL中的hostname。
- yii\web\Request::serverPort:返回80,这是web服务中使用的端口。
4.HTTP头
你可以通过yii\web\Request::headers属性返回的yii\web\HeaderCollection获取HTTP头信息。例如,
//$headers是一个yii\web\HeaderCollection对象 $headers=Yii::$app->request->headers; //返回Acceptheader值 $accept=$headers->get('Accept'); if($headers->has('User-Agent')){/*这是一个User-Agent头*/}
请求组件也提供了支持快速访问常用头的方法,包括:
- yii\web\Request::userAgent:返回User-Agent头。
- yii\web\Request::contentType:返回Content-Type头的值,Content-Type是请求体中MIME类型数据。
- yii\web\Request::acceptableContentTypes:返回用户可接受的内容MIME类型。返回的类型是按照他们的质量得分来排序的。得分最高的类型将被最先返回。
- yii\web\Request::acceptableLanguages:返回用户可接受的语言。返回的语言是按照他们的偏好层次来排序的。第一个参数代表最优先的语言。
假如你的应用支持多语言,并且你想在终端用户最喜欢的语言中显示页面,那么你可以使用语言协商方法yii\web\Request::getPreferredLanguage()。这个方法通过yii\web\Request::acceptableLanguages在你的应用中所支持的语言列表里进行比较筛选,返回最适合的语言。
提示:你也可以使用yii\filters\ContentNegotiator过滤器进行动态确定哪些内容类型和语言应该在响应中使用。这个过滤器实现了上面介绍的内容协商的属性和方法。
5.客户端信息
你可以通过yii\web\Request::userHost和yii\web\Request::userIP分别获取hostname和客户机的IP地址,例如,
$userHost=Yii::$app->request->userHost; $userIP=Yii::$app->request->userIP;
二、响应(Responses)
响应:
当应用完成处理一个请求后,会生成一个yii\web\Response响应对象并发送给终端用户响应对象包含的信息有HTTP状态码,HTTP头和主体内容等,网页应用开发的最终目的本质上就是根据不同的请求构建这些响应对象。
在大多是情况下主要处理继承自yii\web\Response的response应用组件,尽管如此,Yii也允许你创建你自己的响应对象并发送给终端用户,这方面后续会阐述。
在本节,将会描述如何构建响应和发送给终端用户。
1.状态码
构建响应时,最先应做的是标识请求是否成功处理的状态,可通过设置yii\web\Response::statusCode属性,该属性使用一个有效的HTTP状态码。例如,为标识处理已被处理成功,可设置状态码为200,如下所示:
Yii::$app->response->statusCode=200;
尽管如此,大多数情况下不需要明确设置状态码,因为yii\web\Response::statusCode状态码默认为200,如果需要指定请求失败,可抛出对应的HTTP异常,如下所示:
thrownew\yii\web\NotFoundHttpException;
当错误处理器捕获到一个异常,会从异常中提取状态码并赋值到响应,对于上述的yii\web\NotFoundHttpException对应HTTP404状态码,以下为Yii预定义的HTTP异常:
- yii\web\BadRequestHttpException:statuscode400.
- yii\web\ConflictHttpException:statuscode409.
- yii\web\ForbiddenHttpException:statuscode403.
- yii\web\GoneHttpException:statuscode410.
- yii\web\MethodNotAllowedHttpException:statuscode405.
- yii\web\NotAcceptableHttpException:statuscode406.
- yii\web\NotFoundHttpException:statuscode404.
- yii\web\ServerErrorHttpException:statuscode500.
- yii\web\TooManyRequestsHttpException:statuscode429.
- yii\web\UnauthorizedHttpException:statuscode401.
- yii\web\UnsupportedMediaTypeHttpException:statuscode415.
如果想抛出的异常不在如上列表中,可创建一个yii\web\HttpException异常,带上状态码抛出,如下:
thrownew\yii\web\HttpException(402);
2.HTTP头部
可在response组件中操控yii\web\Response::headers来发送HTTP头部信息,例如:
$headers=Yii::$app->response->headers; //增加一个Pragma头,已存在的Pragma头不会被覆盖。 $headers->add('Pragma','no-cache'); //设置一个Pragma头.任何已存在的Pragma头都会被丢弃 $headers->set('Pragma','no-cache'); //删除Pragma头并返回删除的Pragma头的值到数组 $values=$headers->remove('Pragma');
补充:头名称是大小写敏感的,在yii\web\Response::send()方法调用前新注册的头信息并不会发送给用户。
3.响应主体
大多是响应应有一个主体存放你想要显示给终端用户的内容。
如果已有格式化好的主体字符串,可赋值到响应的yii\web\Response::content属性,例如:
Yii::$app->response->content='helloworld!';
如果在发送给终端用户之前需要格式化,应设置yii\web\Response::format和yii\web\Response::data属性,yii\web\Response::format属性指定yii\web\Response::data中数据格式化后的样式,例如:
$response=Yii::$app->response; $response->format=\yii\web\Response::FORMAT_JSON; $response->data=['message'=>'helloworld'];
Yii支持以下可直接使用的格式,每个实现了yii\web\ResponseFormatterInterface类,可自定义这些格式器或通过配置yii\web\Response::formatters属性来增加格式器。
- yii\web\Response::FORMAT_HTML:通过yii\web\HtmlResponseFormatter来实现.
- yii\web\Response::FORMAT_XML:通过yii\web\XmlResponseFormatter来实现.
- yii\web\Response::FORMAT_JSON:通过yii\web\JsonResponseFormatter来实现.
- yii\web\Response::FORMAT_JSONP:通过yii\web\JsonResponseFormatter来实现.
上述响应主体可明确地被设置,但是在大多数情况下是通过操作方法的返回值隐式地设置,常用场景如下所示:
publicfunctionactionIndex() { return$this->render('index'); }
上述的index操作返回index视图渲染结果,返回值会被response组件格式化后发送给终端用户。
因为响应格式默认为yii\web\Response::FORMAT_HTML,只需要在操作方法中返回一个字符串,如果想使用其他响应格式,应在返回数据前先设置格式,例如:
publicfunctionactionInfo() { \Yii::$app->response->format=\yii\web\Response::FORMAT_JSON; return[ 'message'=>'helloworld', 'code'=>100, ]; }
如上所述,触雷使用默认的response应用组件,也可创建自己的响应对象并发送给终端用户,可在操作方法中返回该响应对象,如下所示:
publicfunctionactionInfo() { return\Yii::createObject([ 'class'=>'yii\web\Response', 'format'=>\yii\web\Response::FORMAT_JSON, 'data'=>[ 'message'=>'helloworld', 'code'=>100, ], ]); }
注意:如果创建你自己的响应对象,将不能在应用配置中设置response组件,尽管如此,可使用依赖注入应用通用配置到你新的响应对象。
4.浏览器跳转
浏览器跳转依赖于发送一个LocationHTTP头,因为该功能通常被使用,Yii提供对它提供了特别的支持。
可调用yii\web\Response::redirect()方法将用户浏览器跳转到一个URL地址,该方法设置合适的带指定URL的Location头并返回它自己为响应对象,在操作的方法中,可调用缩写版yii\web\Controller::redirect(),例如:
publicfunctionactionOld() { return$this->redirect('http://example.com/new',301); }
在如上代码中,操作的方法返回redirect()方法的结果,如前所述,操作的方法返回的响应对象会被当总响应发送给终端用户。
除了操作方法外,可直接调用yii\web\Response::redirect()再调用yii\web\Response::send()方法来确保没有其他内容追加到响应中。
\Yii::$app->response->redirect('http://example.com/new',301)->send();
补充:yii\web\Response::redirect()方法默认会设置响应状态码为302,该状态码会告诉浏览器请求的资源临时放在另一个URI地址上,可传递一个301状态码告知浏览器请求的资源已经永久重定向到新的URId地址。
如果当前请求为AJAX请求,发送一个Location头不会自动使浏览器跳转,为解决这个问题,yii\web\Response::redirect()方法设置一个值为要跳转的URL的X-Redirect头,在客户端可编写JavaScript代码读取该头部值然后让浏览器跳转对应的URL。
补充:Yii配备了一个yii.jsJavaScript文件提供常用JavaScript功能,包括基于X-Redirect头的浏览器跳转,因此,如果你使用该JavaScript文件(通过yii\web\YiiAsset资源包注册),就不需要编写AJAX跳转的代码。
5.发送文件
和浏览器跳转类似,文件发送是另一个依赖指定HTTP头的功能,Yii提供方法集合来支持各种文件发送需求,它们对HTTP头都有内置的支持。
- yii\web\Response::sendFile():发送一个已存在的文件到客户端
- yii\web\Response::sendContentAsFile():发送一个文本字符串作为文件到客户端
- yii\web\Response::sendStreamAsFile():发送一个已存在的文件流作为文件到客户端
这些方法都将响应对象作为返回值,如果要发送的文件非常大,应考虑使用yii\web\Response::sendStreamAsFile()因为它更节约内存,以下示例显示在控制器操作中如何发送文件:
publicfunctionactionDownload() { return\Yii::$app->response->sendFile('path/to/file.txt'); }
如果不是在操作方法中调用文件发送方法,在后面还应调用yii\web\Response::send()没有其他内容追加到响应中。
\Yii::$app->response->sendFile('path/to/file.txt')->send();
一些浏览器提供特殊的名为X-Sendfile的文件发送功能,原理为将请求跳转到服务器上的文件,Web应用可在服务器发送文件前结束,为使用该功能,可调用yii\web\Response::xSendFile(),如下简要列出一些常用Web服务器如何启用X-Sendfile功能:
Apache:X-Sendfile Lighttpdv1.4:X-LIGHTTPD-send-file Lighttpdv1.5:X-Sendfile Nginx:X-Accel-Redirect Cherokee:X-SendfileandX-Accel-Redirect
6.发送响应
在yii\web\Response::send()方法调用前响应中的内容不会发送给用户,该方法默认在yii\base\Application::run()结尾自动调用,尽管如此,可以明确调用该方法强制立即发送响应。
yii\web\Response::send()方法使用以下步骤来发送响应:
- 触发yii\web\Response::EVENT_BEFORE_SEND事件.
- 调用yii\web\Response::prepare()来格式化yii\web\Response::data为yii\web\Response::content.
- 触发yii\web\Response::EVENT_AFTER_PREPARE事件.
- 调用yii\web\Response::sendHeaders()来发送注册的HTTP头
- 调用yii\web\Response::sendContent()来发送响应主体内容
- 触发yii\web\Response::EVENT_AFTER_SEND事件.
一旦yii\web\Response::send()方法被执行后,其他地方调用该方法会被忽略,这意味着一旦响应发出后,就不能再追加其他内容。
如你所见yii\web\Response::send()触发了几个实用的事件,通过响应这些事件可调整或包装响应。