HttpFoundation组件

HttpFoundation组件针对HTTP协议定义了一个面向对象层(object-oriented layer)。

在PHP中,reqeust(请求)通过多个超全局变量来呈现($_GET$_POST$_FILE$_COOKIE$_SESSION...),而response(响应)则被若干函数所生成(echoheadersetcookie...)。

Symfony HttpFoundation组件用一个面向对象层替换了PHP默认的全局变量及函数。

安装 

你可以通过下述两种方式安装:

然后,包容vendor/autoload.php文件,以开启Composer提供的自动加载机制。否则,你的程序将无法找到这个Symfony组件的类。

请求(Request) 

创建一个request(对象)的最常用方式,是基于当前的PHP全局变量,利用createFromGlobals()方法来完成。

1
2
3
use Symfony\Component\HttpFoundation\Request;
 
$request = Request::createFromGlobals();

这差不多相当于调用冗长但却更灵活的__construct()构造方法:

1
2
3
4
5
6
7
8
$request = new Request(
    $_GET,
    $_POST,
    array(),
    $_COOKIE,
    $_FILES,
    $_SERVER
);

访问请求数据 

Request对象持有客户端请求的相关信息。这些信息可以通过几个公有属性访问到:

  • request:等同于$_POST

  • query:等同于$_GET$request->query->get('name'));

  • cookies:等同于$_COOKIE

  • attributes:无对应物——它被你的程序用来存储其他数据(参考下文这里);

  • files:等同于$_FILES

  • server:等同于$_SERVER

  • headers:多数时候等同于一组$_SERVER$request->headers->get('User-Agent')

每个属性都是一个ParameterBag参数包实例(或其子类),参数包是一个数据持有类:

所有ParameterBag参数包实例,都有如下用于取出和更新其数据的方法:

all()
返回参数数组。
keys()
返回参数的键。
replace()
用一组新值替换当前参数(数组)。
add()
添加参数。
get()
用参数名(name)来返回参数。
set()
用参数名(name)来设置参数。
has()
如果参数被定义,返回true
remove()
删除一个参数。

ParameterBag参数包实例还有如下用于过滤输入值(input value)的方法:

getAlpha()
返回按字母排序的参数值;
getAlnum()
返回按字母和数字排序的参数值;
getBoolean()
返回被转换为布尔值的参数值;
getDigits()
返回参数值的数位(digits);
getInt()
返回被转换为整型的参数值;
filter()
通过使用PHP的filter_var 函数来过滤参数。

所有上述getter方法接收两个参数(arguments):第一个是参数名(parameter name),第二个是当参数不存在时的默认返回值:

1
2
3
4
5
6
7
8
9
10
// the query string is '?foo=bar'
 
$request->query->get('foo');
// returns 'bar'
 
$request->query->get('bar');
// returns null
 
$request->query->get('bar', 'baz');
// returns 'baz'

当PHP引入一个request query时,它在处理诸如foo[bar]=bar这种request parameters时使用一种特殊的方式,即“创建数组”。所以你要得到foo参数时拿到的是一个含有bar元素的数组:

1
2
3
4
5
6
7
8
9
10
// the query string is '?foo[bar]=baz'
 
$request->query->get('foo');
// returns array('bar' => 'baz')
 
$request->query->get('foo[bar]');
// returns null
 
$request->query->get('foo')['bar'];
// returns 'baz'

得益于公有的(public)attributes属性,你可以把附加数据存到request(属性)中,request本身也是一个ParameterBag参数包实例。这种方法往往被用于附着(attach)Request对象的相关信息,而这些信息要在你程序中的多个不同部分被访问到。

最后,连同请求本体(request body)一起被发送来的原始数据(raw data)可以通过getContent()来访问到:

1
$content = $request->getContent();

例如,这在处理一个“由[使用了HTTP POST方法的]远程服务发送到程序”的JSON字符串时非常有用。

识别一个请求 

在程序中,你需要一种识别请求的方式。多数时候,这可以通过请求的“path info”来完成,它可以通过getPathInfo()来访问到:

1
2
3
4
5
// for a request to http://example.com/blog/index.php/post/hello-world
// the path info is "/post/hello-world"
// 对于http://example.com/blog/index.php/post/hello-world这个请求,
// path info是 "/post/hello-world"
$request->getPathInfo();

模拟一个请求 

除了基于PHP全局变量创建请求之外,你还可以模拟一个请求:

1
2
3
4
5
$request = Request::create(
    '/hello-world',
    'GET',
    array('name' => 'Fabien')
);

create()方法基于一个URI、一个METHOD以及一些参数(query parameter还是request参数,取决于HTTP METHOD)来创建request(对象)。当然,你也可以覆写所有其他的变量(默认情况是,Symfony为所有的PHP全局变量创建了有意义的默认值)。

基于这种(模拟的)请求,你可以通过overrideGlobals()方法覆写PHP全局变量:

1
$request->overrideGlobals();

你可以通过duplicate()方法对已经存的请求进行复制,或者,调用一次initialize()方法来改变一堆参数。

访问Session 

如果你在请求中附带了session,你可以通过getSession()方法访问到它;hasPreviousSession()则告诉你请求中“是否包含了一个始于之前某个请求”的session。

访问Accept-*Headers数据 

通过以下方法,你可以很容易的获得提取自Accept-*头的基础数据:

getAcceptableContentTypes()
返回可接受的Content types之列表,quality降序排序。
getLanguages()
返回可接受的languages之列表,quality降序排序。
getCharsets()
返回可接受的charsets之列表,quality降序排序。
getEncodings()
返回可接受的encodings之列表,quality降序排序。

如果你需要全面控制来自AcceptAccept-LanguageAccept-CharsetAccept-Encoding的解析数据,可以使用AcceptHeader工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\HttpFoundation\AcceptHeader;
 
$accept = AcceptHeader::fromString($request->headers->get('Accept'));
if ($accept->has('text/html')) {
    $item = $accept->get('text/html');
    $charset = $item->getAttribute('charset', 'utf-8');
    $quality = $item->getQuality();
}
 
// Accept header items are sorted by descending quality
// Accept头元素被以quality的降序来排序
//(译注:"q"参数详见 https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html)
$accepts = AcceptHeader::fromString($request->headers->get('Accept'))
    ->all();

访问其他数据 

Request类还有很多其他方法可供你用来访问请求中的信息。看一看the Request API以了解更多。

覆写请求 

Request类不应被覆写,因为它是一个数据对象,用来呈现一条HTTP信息(HTTP message)。但是当它来自于程序遗产(legacy system)时,添加方法或改变一些默认行为可能是有用的。这种时候,注册一个PHP回调(callable)来创建一个Request类的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use Symfony\Component\HttpFoundation\Request;
 
Request::setFactory(function (
    array $query = array(),
    array $request = array(),
    array $attributes = array(),
    array $cookies = array(),
    array $files = array(),
    array $server = array(),
    $content = null
) {
    return SpecialRequest::create(
        $query,
        $request,
        $attributes,
        $cookies,
        $files,
        $server,
        $content
    );
});
 
$request = Request::createFromGlobals();

响应(Response) 

一个Response对象持有“从给定请求发送回客户端”的全部信息。其构造器接收三个参数:响应内容、状态码(status code),以及一个HTTP头的数组:

1
2
3
4
5
6
7
use Symfony\Component\HttpFoundation\Response;
 
$response = new Response(
    'Content',
    Response::HTTP_OK,
    array('content-type' => 'text/html')
);

这些(构造参数所对应的)信息可以在响应创建之后进行处理:

1
2
3
4
5
6
7
$response->setContent('Hello World');
 
// the headers public attribute is a ResponseHeaderBag
// 公有属性headers,是一个ResponseHeaderBag实例
$response->headers->set('Content-Type', 'text/plain');
 
$response->setStatusCode(Response::HTTP_NOT_FOUND);

当设置响应的Content-Type时,你可以顺带设置字符集,但更好的办法是通过setCharset()方法来设置:

1
$response->setCharset('ISO-8859-1');

注意,默认情况下,Symfony假定你的响应是UTF-8编码。

发送响应 

在发送响应之前,你可以通过调用prepare()方法,确保它符合HTTP协议:

1
$response->prepare($request);

发送响应到客户端时,只需调用send方法:

1
$response->send();

设置Cookies 

响应的cookies,可以通过headers公有属性来操作:

1
2
3
use Symfony\Component\HttpFoundation\Cookie;
 
$response->headers->setCookie(new Cookie('foo', 'bar'));

setCookie()方法接收一个Cookie实例作为参数。

你可以通过clearCookie()方法清除一个cookie。

管理HTTP缓存 

Response类拥有丰富的方法,用来操作“和缓存有关的”HTTP头:

setCache()方法可以在一个方法调用中完成“最为常见”的缓存信息设置:

1
2
3
4
5
6
7
8
$response->setCache(array(
    'etag'          => 'abcdef',
    'last_modified' => new \DateTime(),
    'max_age'       => 600,
    's_maxage'      => 600,
    'private'       => false,
    'public'        => true,
));

若要检查Response Validator(EtagLast-Modified)是否匹配客户端请求中指定的一个条件值,使用isNotModified()方法:

1
2
3
if ($response->isNotModified($request)) {
    $response->send();
}

如果响应没有被修改,它会设置状态码为304,然后删除真正的响应内容(response content)。

重定向用户 

要重定向客户端到另外一个URL,你可以使用RedirectResponse类:

1
2
3
use Symfony\Component\HttpFoundation\RedirectResponse;
 
$response = new RedirectResponse('http://example.com/');

流化一个响应 

StreamedResponse类允许你把响应流化到客户端。响应内容以一个PHP回调(callable)来呈现,而不是一个字符串:

1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\HttpFoundation\StreamedResponse;
 
$response = new StreamedResponse();
$response->setCallback(function () {
    var_dump('Hello World');
    flush();
    sleep(2);
    var_dump('Hello World');
    flush();
});
$response->send();

flush()函数并未清除缓存(buffering)。如果ob_start()之前被调用,或者output_buffering这个php.ini选项被开启,你必须在flush()之前调用ob_flush()

此外,PHP并非唯一对输出(output)进行缓存的层。你的服务器也可能基于自身配置而缓存。而且,如果你使用了FastCGI,那么缓存根本无法被禁止。

文件操作 

当发送一个文件时,你必须在响应中添加一个Content-Disposition头。虽然,为最基本的file downloads(文件下载)创建这样一个头是非常简单的,但使用一个non-ASCII文件名是更重要的。makeDiposition()把繁重的工作抽象成(abstract)一个简单的API:

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
 
$fileContent = ...; // the generated file content 生成文件内容
$response = new Response($fileContent);
 
$disposition = $response->headers->makeDisposition(
    ResponseHeaderBag::DISPOSITION_ATTACHMENT,
    'foo.pdf'
);
 
$response->headers->set('Content-Disposition', $disposition);

或者,如果你正处理一个静态文件(static file),你可以使用一个 BinaryFileResponse响应:

1
2
3
4
use Symfony\Component\HttpFoundation\BinaryFileResponse;
 
$file = 'path/to/file.txt';
$response = new BinaryFileResponse($file);

BinaryFileResponse响应将自动处理请求中的RangeIf-Range头。它也支持X-Sendfile(参考NginxApache)。要利用此功能,你需要决定X-Sendfile-Type头是否应被信任,如果信任,那么就调用trustXSendfileTypeHeader()

1
BinaryFileResponse::trustXSendfileTypeHeader();

使用BinaryFileResponse响应,你仍然能够设置被发送文件的Content-Type头,或者改变它的Content-Disposition

1
2
3
4
5
6
// ...
$response->headers->set('Content-Type', 'text/plain');
$response->setContentDisposition(
    ResponseHeaderBag::DISPOSITION_ATTACHMENT,
    'filename.txt'
);

使用deleteFileAfterSend()方法的话,在请求发送之后对文件进行删除是可能的。但是请注意它在设置了X-Sendfile头时将不工作。

若你恰好 在相同的请求中创建了文件,这个文件在被发送时可能 不包含任何内容。这是因为缓存文件状态对文件容量返回了0导致的。为了修复此问题,调用clearstatcache(false, $file)方法,使用binary file的路径(path)作为其参数。

创建JSON响应 

通过Response类,设置合适的content和headers,即可创建任何类型的响应。一个JSON响应看起来像下面这样:

1
2
3
4
5
6
7
use Symfony\Component\HttpFoundation\Response;
 
$response = new Response();
$response->setContent(json_encode(array(
    'data' => 123,
)));
$response->headers->set('Content-Type', 'application/json');

框架里有一个好用的JsonResponse类,可以简化类似操作:

1
2
3
4
5
6
use Symfony\Component\HttpFoundation\JsonResponse;
 
$response = new JsonResponse();
$response->setData(array(
    'data' => 123
));

这可以把你的data数组转换成JSON,然后设置其Content-Type头为application/json

为了防止XSS|JSON Hijacking(JSON劫持),你应该传入一个关联数组作为JsonResponse的最外层数组,而不是一个索引数组,这样一来最终结果就是一个对象(比如,[{"object": "not inside an array"}])而不是数组(如,[{"object": "inside an array"}])。参考OWASP guidelines了解更多。

JASONP Callback 

如果你正使用JASONP,你可以设置“数据应被传入”的callback function(回调函数):

1
$response->setCallback('handleResponse')

这时,Content-Type头将是text-javascript,响应内容(response content)可能是下面这样:

1
handleResponse({'data': 123});

Session 

Session有自己的独立章节,参考中文组件之Session管理

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)