使用 Symfony 组件创建自己的 PHP 框架(第二部分:HttpFoundation 组件)

使用 Symfony 组件创建自己的 PHP 框架(第二部分:HttpFoundation 组件)

Chris Yue 3 comments
Posts

英文原文地址:https://symfony.com/doc/current/create_framework/http_foundation.html

在开始重构我们的代码之前,我打算先再谈谈为什么您最好使用一个框架来替代用 PHP 直接书写这种方式来创建一个应用程序。即使写一个很小的代码片段,使用框架也是一个好主意,而使用 sf 组件库来创建一个框架比直接写一个框架更好。

这里我不会讨论在有多个开发者共同开发的大项目中,那些经典的或者明显的使用框架的好处。网上已经有很多关于这个话题的资源和信息。

即使昨天我们写的『应用程序』非常的简单,但也会遇到很多问题:

<?php

// framework/index.php

$input = $_GET['name'];

printf('Hello %s', $input);

首先,如果 URL 里面没有 GET 参数,PHP 会抛出一个警告,让我们修正它:

<?php

// framework/index.php

$input = isset($_GET['name']) ? $_GET['name'] : 'World';

printf('Hello %s', $input);

其次,这段代码并不安全。你知道吗?即使是这么简单的代码也有普遍的网络安全问题 XSS(跨域脚本攻击,译者注:即攻击者构造一个 name 参数带有 javascript 脚本内容的 URL 让另外一个人访问,网页打开后脚本就会在被攻击人的浏览器上运行)而易被攻击。下面是一个更安全的版本:

<?php

$input = isset($_GET['name']) ? $_GET['name'] : 'World';

header('Content-Type: text/html; charset=utf-8');

printf('Hello %s', htmlspecialchars($input, ENT_QUOTES, 'UTF-8'));

正如你所见,使用 htmlspecialchars 来加强安全性的话,既感觉很烦又容易疏漏,这便是为什么要使用类似 Twig 等模版引擎的原因,它的自动转义功能是默认开启的。即使关闭自动转义,使用自带 e 方法也会相对使用 htmlspecialchars 方便不少

正如你所见到的那样,为了不出现 PHP 警告以及加强安全性,我们的代码已经不像当初那样简单了。

除了安全性,这段代码也很难测试。虽然这段代码也没什么好测试的,但即使是这么简单的代码,我也觉得很难优雅而自然的进行测试。这是我利用 PHPUnit 来写的一个测试:

<?php

// framework/test.php

class IndexTest extends \PHPUnit_Framework_TestCase
{
    public function testHello()
    {
        $_GET['name'] = 'Fabien';

        ob_start();
        include 'index.php';
        $content = ob_get_clean();

        $this->assertEquals('Hello Fabien', $content);
    }
}

如果我们的项目再大一些,我们还会发现更多的问题。如果你非常想知道还有哪些问题,请阅读 sf 文档的《sf VS 纯 PHP》部分

从这几点来看,如果你还觉得安全性和可测性不是使用框架的好理由,你可以不用继续往下阅读了。

当然,使用框架给你带来的不只是安全性和可测试性。请记住你选择的框架必须能够让你更快的写出更好的代码来

使用 HttpFoundation 组件迈向面向对象之路

写 web 程序都是有关 HTTP 交互的,所以我们写框架的基本原则都是围绕着 HTTP 协议展开的

HTTP 协议描述了客户端(比如说浏览器)和服务器端(比如网络服务器)的交互方式。客户端和服务器端的对话是通过良好定义的“消息”——即请求和响应的传递来实现的。客户端向服务器发起一个请求,服务器根据这个请求再向客户端返回一个响应

在 PHP 的世界里,请求由全局变量($_GET, $_POST, $_FILE 等)来表示,响应又由一些函数来生成(echo, header, setcookie 等,译者注:echo 严格意义上来说不是一个函数,不过你懂作者的意思就行了)

写出更好代码的第一步最好是使用面向对象来实现。这是 sf 的 HttpFoundation 组件的主要目的:利用面向对象层来代替 PHP 默认使用的全局变量。

要使用此组件,使用 composer 给我们的项目添加对此组件的依赖:

composer require symfony/http-foundation

运行完此命令之后 composer 会自动下载 sf 的 HttpFoundation 组件,并且将他安装在 vendor 目录下。同时会创建 composer.jsoncomposer.lock 两个文件。composer.json 会包含以下内容,显示当前项目的依赖:

{
    "require": {
        "symfony/http-foundation": "^3.0"
    }
}

内容可能会因为版本不同而有些不一样

命名规则和自动加载

安装完一个新的依赖之后,composer 还会自动创建一个 vendor/autoload.php 文件,用来让所有被管理的类都能实现自动加载。没有自动加载,你在使用一个类之前,必须引入定义此类的文件。幸好现在有 PSR-4 标准,可以让 composer 和 PHP 来帮我们处理文件加载问题。

现在,让我们用请求类(Request class)和响应类(Response class)来重写我们的程序:

<php

// framework/index.php

require_once __DIR__.'/vendor/autoload.php';

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

$request = Request::createFromGlobals();

$input = $request->get('name', 'World');

$response = new Response(sprintf(
    'Hello %s', 
    htmlspecialchars($input, ENT_QUOTES, 'UTF-8')
));

$response->send();

createFromGlobals 方法会根据当前PHP的全局变量生成一个 Request 对象。

send 方法返回 Response 对象里的内容到客户端(他会先根据内容发送 HTTP 头信息)。

其实在调用 send 方法之前,我们应该先调用一下 prepare 方法($response->prepare($request))来确认发送一个与标准 HTTP 协议兼容的相应。举个例子,如果客户端对一个页面发送 HEAD 请求,那么服务器是不应该返回响应正文的(译者注:HEAD 请求类似 GET 请求,除了要求服务器只返回头信息)。

对比之前的代码,你现在能完全控制 HTTP 消息。你能创建任何任何你想创建的请求,你也能发送任何你想返回的响应。

我们并没有显式设置 Content-typeUTF-8,因为 Response 对象默认发送的就是 UTF-8 编码的内容。

得益于优雅而简单的 API,你能很轻易的通过 Request 对象获得请求的各种信息:

<?php

// 请求的URI (e.g. /about)
$request->getPathInfo();

// 分别得到GET参数或POST参数
$request->query->get('foo'); // GET
$request->request->get('bar', '如何没有bar的默认值'); // POST

// 得到服务器变量
$request->server->get('HTTP_HOST');

// 得到上传文件对象
$request->files->get('foo');

// 得到cookie值
$request->cookies->get('PHPSESSID');

// 得到http请求头信息
$request->headers->get('host');
$request->headers->get('content_type');

$request->getMethod();    // GET, POST, PUT, DELETE, HEAD
$request->getLanguages(); // 得到客户端接收语言数组

你也能够模拟一个请求:

$request = Request::create('/index.php?name=Fabien');

你也能利用 Response 对象很轻易的改变响应:

<?php

$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);

要 debug 一个相应对象,使用 (string) 让其变成一个字符串,他将返回 HTTP 响应的全部信息(包括头信息和正文)

最后也是最重要的一点,这些类以及 sf 组件库其他的类,安全性方面都被一家专门做安全评审的公司评估过(译者注:这里说的公司应该是指 SektionEins,德国一家专门做安全评估的公司),而且作为开源项目就意味着许多志愿程序员已经将许多潜在的安全问题解决了。你上一次给你的框架做专业的安全评估是什么时候?(译者注:意思是说一般人自己在家鼓捣的框架不可能去搞什么专业的安全评估的)

即使像获取用户 IP 这么简单的操作,也会出现安全隐患:

<?php

if ($myIp == $_SERVER['REMOTE_ADDR']) {
    // 被信任的IP!给一些权限!
}

当你在你的生产 web 服务器前加了反相代理的时候,你会发现这段代码不会正常的工作。为了让你的代码能够在你的生产服务器和开发服务器都能够使用,你需要把代码改成下面这个样子:

<?php

if ($myIp == $_SERVER['HTTP_X_FORWARDED_FOR'] 
    || $myIp == $_SERVER['REMOTE_ADDR']) {
    // 被信任的 IP!给一些权限!
}

而使用 Request::getClientIp() 方法将让你始终可以得到正确的用户 IP(无论请求过来之前经过了多少代理服务器)

<?php

$request = Request::createFromGlobals();

if ($myIp == $request->getClientIp()) {
    // 被信任的 IP!给一些权限!
}

另外还有一个好处:默认情况下它是安全的,这里的安全我指的是:在考虑存在代理服务器(包括反向代理)的情况下,$_SERVER['HTTP_X_FORWARDED_FOR'] 这个参数并不可信,因为假如客户端不使用代理服务器,客户端用户是可以假冒它的。但在显式调用了 setTrustedProxies 方法之后再使用 getClientIp 方法就不会存在这个问题

<?php

Request::setTrustedProxies(array('10.0.0.1'));

if ($myIp == $request->getClientIp()) {
    // 被信任的 IP!给一些权限!
}

此刻 getClientIp 方法在各种情况下都会安全的工作,你能在你所有的项目里使用它,无论你用什么样的服务器配置,他都会安全而正确的工作。这便是使用框架的目的之一,如果你要从头到尾自己琢磨一个框架,这些问题你都得自己去考虑。所以为何不使用一个已经好用的技术呢?

如果你想知道关于 HttpFoundation 组件更多的信息,你可以查看它的 API,或者查看 sf 网站上写得非常专业的文档

不管你信不信,我们已经写好我们的第一个框架了。如果你愿意的话你也可不必再继续往下阅读,因为使用 sf 的 HttpFoundation 组件已经能让你写出更好更适合测试的代码了,另外它也能让你的代码写得更快,因为那些每次开发都需要注意的问题它都已经帮你解决了。

事实上,像 Drupal 等一些项目已经开始采用 HttpFoundation 组件了(从第 8 个版本开始)。如果这个组件能在这些项目里正常运行,那么它也可以在你的项目里正常运行。所以……不要重复发明轮子。

我差点忘了还有一个使用 HttpFoundation 组件的好处:它会让你在那些使用过它的项目和框架中有更好的互操作性(interoperability)(目前这样的项目有 Symfony 框架, Drupal 8, phpBB 4, Laraval, ezPublish 5, Silex 等)

返回阅读第一部分 | 继续阅读第三部分

使用 Symfony 组件创建自己的 PHP 框架(第二部分:HttpFoundation 组件) by Chris Yue is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

微信赞赏码

写作累,服务器也越来越贵
求分担,祝愿好人一生平安
天使打赏人

3 Comments

point

3月 9, 2016 在 8:27 下午

trustProxyData,此函数不存在?

 回复

    Chris Yue

    3月 13, 2016 在 10:01 下午

    什么版本?

     

    gt9000

    8月 5, 2016 在 1:43 下午
    // 文件:vendor/symfony/http-foundation/Symfony/Component\HttpFoundation\Request.php
    /**
    * Trusts $_SERVER entries coming from proxies.
    *
    * @deprecated Deprecated since version 2.0, to be removed in 2.3. Use setTrustedProxies instead.
    */
    public static function trustProxyData()
    {
        self::$trustProxy = true;
    }

    我翻了下源码,里面写着 trustProxyData 这个方法,在 2.0 版本已经不被建议使用了。在 2.3 版本被删除。如果是 2.3 以后的版本,请使用 setTrustedProxies 替代。

     

发表评论

81 − = 80

2021年7月
 1234
567891011
12131415161718
19202122232425
262728293031  
Composer CSS Firefox industrial metal JavaScript LeetCode Linux MySQL NGINX nu metal OAuth PHP PHP7 Shell Symfony Ubuntu Vim 全栈 到底系列 命令行 安全 教程 框架 算法 翻译