HttpFoundation 组件
在深入框架创建过程之前,让我们先退后一步,看看为什么要使用框架,而不是保持你原有的旧式 PHP 应用程序。为什么使用框架实际上是一个好主意,即使是最简单的代码片段,以及为什么在 Symfony 组件之上创建你的框架比从头开始创建一个框架更好。
注意
我们不会讨论在使用框架处理有多个开发人员的大型应用程序时的传统优势;互联网上已经有很多关于这个主题的优秀资源。
即使我们在上一章中编写的“应用程序”足够简单,它仍然存在一些问题
1 2 3 4
// framework/index.php
$name = $_GET['name'];
printf('Hello %s', $name);
首先,如果 URL 查询字符串中未定义 name
查询参数,你将收到 PHP 警告;所以让我们修复它
1 2 3 4
// framework/index.php
$name = $_GET['name'] ?? 'World';
printf('Hello %s', $name);
然后,这个应用程序是不安全的。你能相信吗?即使这个简单的 PHP 代码片段也容易受到最广泛的互联网安全问题之一,XSS(跨站脚本攻击)。这是一个更安全的版本
1 2 3 4 5
$name = $_GET['name'] ?? 'World';
header('Content-Type: text/html; charset=utf-8');
printf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8'));
注意
你可能已经注意到,使用 htmlspecialchars
保护你的代码是乏味且容易出错的。这就是为什么使用像 Twig 这样的模板引擎(默认启用自动转义)可能是一个好主意的原因之一(并且使用简单的 e
过滤器,显式转义也更轻松)。
正如你自己所见,如果我们想要避免 PHP 警告/通知并使代码更安全,我们最初编写的简单代码不再那么简单。
除了安全性之外,这段代码可能难以测试。即使没有什么可测试的,但令我惊讶的是,为最简单的 PHP 代码片段编写单元测试是不自然的,并且感觉很糟糕。这是一个针对上述代码的临时 PHPUnit 单元测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// framework/test.php
use PHPUnit\Framework\TestCase;
class IndexTest extends TestCase
{
public function testHello(): void
{
$_GET['name'] = 'Fabien';
ob_start();
include 'index.php';
$content = ob_get_clean();
$this->assertEquals('Hello Fabien', $content);
}
}
注意
如果我们的应用程序稍微大一点,我们就能发现更多问题。如果你对它们感到好奇,请阅读本书的 Symfony 与 Flat PHP 章节。
此时,如果你仍然不相信安全性和测试确实是停止以旧方式编写代码并转而采用框架(无论在这种上下文中采用框架意味着什么)的两个非常好的理由,你可以现在停止阅读本书,然后回到你之前正在处理的任何代码。
注意
使用框架应该给你带来的不仅仅是安全性和可测试性,但更重要的是要记住,你选择的框架必须允许你更快地编写更好的代码。
使用 HttpFoundation 组件进行面向对象编程
编写 Web 代码是关于与 HTTP 交互。因此,我们框架的基本原则应该围绕 HTTP 规范。
HTTP 规范描述了客户端(例如浏览器)如何与服务器(我们的应用程序通过 Web 服务器)交互。客户端和服务器之间的对话由明确定义的消息、请求和响应指定:客户端向服务器发送请求,服务器根据此请求返回响应。
在 PHP 中,请求由全局变量($_GET
、$_POST
、$_FILE
、$_COOKIE
、$_SESSION
...)表示,响应由函数(echo
、header
、setcookie
, ...)生成。
迈向更好代码的第一步可能是使用面向对象的方法;这是 Symfony HttpFoundation 组件的主要目标:用面向对象的层替换默认的 PHP 全局变量和函数。
要使用此组件,请将其添加为项目的依赖项
1
$ composer require symfony/http-foundation
运行此命令还将自动下载 Symfony HttpFoundation 组件,并将其安装在 vendor/
目录下。还将生成 composer.json
和 composer.lock
文件,其中包含新的需求。
现在,让我们通过使用 Request
和 Response
类来重写我们的应用程序
1 2 3 4 5 6 7 8 9 10 11 12 13
// framework/index.php
require_once __DIR__.'/vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
$request = Request::createFromGlobals();
$name = $request->query->get('name', 'World');
$response = new Response(sprintf('Hello %s', htmlspecialchars($name, ENT_QUOTES, 'UTF-8')));
$response->send();
createFromGlobals()
方法基于当前的 PHP 全局变量创建一个 Request
对象。
send()
方法将 Response
对象发送回客户端(它首先输出 HTTP 标头,然后是内容)。
提示
在调用 send()
之前,我们应该调用 prepare()
方法($response->prepare($request);
)以确保我们的 Response 符合 HTTP 规范。例如,如果我们使用 HEAD
方法调用页面,它将删除 Response 的内容。
与之前的代码的主要区别在于,你可以完全控制 HTTP 消息。你可以创建任何你想要的请求,并且你可以随时负责发送响应。
注意
我们没有在重写的代码中显式设置 Content-Type
标头,因为 Response 对象的字符集默认为 UTF-8
。
使用 Request
类,你可以通过一个简洁而简单的 API 轻松获得所有请求信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// the URI being requested (e.g. /about) minus any query parameters
$request->getPathInfo();
// retrieves GET and POST variables respectively
$request->query->get('foo');
$request->getPayload()->get('bar', 'default value if bar does not exist');
// retrieves SERVER variables
$request->server->get('HTTP_HOST');
// retrieves an instance of UploadedFile identified by foo
$request->files->get('foo');
// retrieves a COOKIE value
$request->cookies->get('PHPSESSID');
// retrieves a HTTP request header, with normalized, lowercase keys
$request->headers->get('host');
$request->headers->get('content-type');
$request->getMethod(); // GET, POST, PUT, DELETE, HEAD
$request->getLanguages(); // an array of languages the client accepts
你还可以模拟请求
1
$request = Request::create('/index.php?name=Fabien');
使用 Response
类,你可以调整响应
1 2 3 4 5 6 7 8
$response = new Response();
$response->setContent('Hello world!');
$response->setStatusCode(200);
$response->headers->set('Content-Type', 'text/html');
// configure the HTTP cache headers
$response->setMaxAge(10);
提示
要调试响应,请将其转换为字符串;它将返回响应的 HTTP 表示形式(标头和内容)。
最后但并非最不重要的一点是,这些类,就像 Symfony 代码中的每个其他类一样,都经过一家独立公司对其安全性问题进行了审核。并且作为一个开源项目,也意味着世界各地的许多其他开发人员已经阅读了代码,并且已经修复了潜在的安全问题。你上次为你的自制框架订购专业安全审核是什么时候?
即使是像获取客户端 IP 地址这样简单的事情也可能是不安全的
1 2 3
if ($myIp === $_SERVER['REMOTE_ADDR']) {
// the client is a known one, so give it some more privilege
}
在你在生产服务器前面添加反向代理之前,它都可以完美地工作;此时,你将必须更改你的代码,使其在你的开发机器(你没有代理)和你的服务器上都能工作
1 2 3
if ($myIp === $_SERVER['HTTP_X_FORWARDED_FOR'] || $myIp === $_SERVER['REMOTE_ADDR']) {
// the client is a known one, so give it some more privilege
}
使用 Request::getClientIp()
方法从一开始就会给你正确的行为(并且它将涵盖你拥有链式代理的情况)
1 2 3 4 5
$request = Request::createFromGlobals();
if ($myIp === $request->getClientIp()) {
// the client is a known one, so give it some more privilege
}
还有一个额外的好处:默认情况下它是安全的。这是什么意思?$_SERVER['HTTP_X_FORWARDED_FOR']
值不可信,因为它可以在没有代理时被最终用户操纵。因此,如果你在没有代理的情况下在生产环境中使用此代码,则很容易滥用你的系统。使用 getClientIp()
方法则不是这种情况,因为你必须通过调用 setTrustedProxies()
显式信任你的反向代理
1 2 3 4 5
Request::setTrustedProxies(['10.0.0.1'], Request::HEADER_X_FORWARDED_FOR);
if ($myIp === $request->getClientIp()) {
// the client is a known one, so give it some more privilege
}
因此,getClientIp()
方法在所有情况下都能安全地工作。你可以在所有项目中使用它,无论配置如何,它都会正确且安全地运行。这是使用框架的目标之一。如果你要从头开始编写框架,你将不得不自己考虑所有这些情况。为什么不使用已经可以工作的技术呢?
注意
如果你想了解有关 HttpFoundation 组件的更多信息,你可以查看 Symfony\Component\HttpFoundation
API 或阅读其专门的文档。
信不信由你,但我们有了我们的第一个框架。如果你愿意,现在就可以停止了。仅使用 Symfony HttpFoundation 组件已经可以让你编写更好、更可测试的代码。它还可以让你更快地编写代码,因为许多日常问题已经为你解决。
事实上,像 Drupal 这样的项目已经采用了 HttpFoundation 组件;如果它对他们有效,那么它可能对你也有效。不要重新发明轮子。
我几乎忘记谈到一个额外的好处:使用 HttpFoundation 组件是所有框架和使用它的应用程序(如 Symfony、Drupal 8、phpBB 3、Laravel 和 ezPublish 5,以及更多)之间更好互操作性的开始。