基于 Symfony 组件封装 HTTP 请求响应类

引言

上篇教程学院君给大家介绍了命名空间以及如何基于 Composer 来管理命名空间与 PHP 脚本路径的映射,自此以后,我们将基于这套机制来实现 PHP 类的自动加载和函数引入。

接下来,学院君会以前面作业中编写的博客系统为例,构建一个简单的 PHP MVC 框架。我们将演示路由器、控制器、视图模板、模型类、Session 等基本组件的实现,并反过来基于这些组件完成博客系统的 CRUD(增删改查)功能。

我们知道,对于 Web 框架而言,最基础的功能就是处理请求、返回响应,这一点我们在前面 PHP HTTP 编程中已经演示过,不过如果基于 PHP 自带的请求信息获取和响应设置机制,代码是面向过程风格的,不够优雅,要想基于面向对象风格解析请求、设置响应,可以基于 PHP 原生代码封装请求类和响应类。

在开始构建 Web 框架之前,我们先来封装请求和响应类以便于后面使用。

Symfony HTTP Foundation 组件

关于这两个类的封装,我们可以基于 Symfony 提供的 HTTP Foundation 组件来实现,Symfony 本身是一个著名的 PHP MVC 框架,它提供了丰富的 PHP 组件集,可以独立于 Symfony 框架之外使用,你可以在这里看到 Symfony 提供的全部组件集:Symfony Components,这是 Symfony 作为框架之外对 PHP 生态的巨大贡献。

限于篇幅,我们这里简单介绍下 Symfony HTTP Foundation 这个组件,它包含了对 PHP HTTP 请求、响应和会话功能的封装,通过这些封装类实例提供的方法,我们可以以面向对象的风格进行 HTTP 编程,而不再需要到处使用 $_SERVER$_REQUEST$_FILES$_SESSION 之类的超全局变量,从而方便代码的风格统一和后期维护。以 Request 类为例,它封装了 $_GET$_POST$_COOKIE$_SERVER$_FILES 等所有超全局变量中的信息,在设置/获取的时候,使用特定的 API 方法即可,而不需要操作这些超全局变量。关于这些封装类的使用,参考 Symfony 官方文档即可:https://symfony.com/doc/current/components/http_foundation.html,这里不详细介绍。

要引入 Symfony HTTP Foundation 组件,需要通过 Composer 在 blog 根目录下运行如下命令下载这个扩展包:

composer require symfony/http-foundation

下载完成后的扩展包会保存到 vendor/symfony/http-foundation 目录下,另外,也会在 composer.json 中记录这个扩展包的名称和版本:

"require": {
    "symfony/http-foundation": "^5.1"
},

重新组织博客项目目录结构

此外,我们还要基于命名空间重新组件 blog 项目代码:

-w561

注:详细代码参见 https://github.com/nonfu/master-laravel-code/tree/v0.4/practice/blog

我们将所有应用 PHP 代码都转移到了 app 目录下,并且为其设置了命名空间 App,将对外公开的静态资源文件和入口文件 index.php 转移到了 public 目录,而将视图模板文件都转移到了 views 目录下。

基于 Symfony 基类封装请求响应类

注意到 app/http 这个子目录,我们将应用需要用到的 RequestResponseSession 类都放到这个目录下:

-w865

这三个类分别继承自 Symfony HTTP Foudation 组件的 RequestResponseSession 基类,这里,我们新增子类实现的目的是为了便于添加自定义逻辑。在 Request 子类中新增了两个方法,用于初始化 HTTP 请求和获取请求路径,而 ResponseSession 目前没有定义任何新增方法:

<?php
namespace App\Http;
use \Symfony\Component\HttpFoundation\Response as BaseResponse;

class Response extends BaseResponse
{

}

编写好了上述几个子类后,在 composer.json 中配置需要维护命名空间路径映射的目录:

"autoload": {
    "classmap": [
        "app"
    ]
}

然后运行 composer dump-auto 让新增的命名空间类映射关系生效。至此,我们就完成了请求和响应类的封装。

使用请求和响应类

最后,我们在入口文件 public/index.php 中使用封装后的请求和响应类重构请求处理逻辑:

<?php
require_once __DIR__ . '/../vendor/autoload.php';
$container = require_once __DIR__ . '/../app/bootstrap.php';

$request = \App\Http\Request::capture();

$store = $container->resolve(\App\Store\StoreContract::class);
$connection = $store->newConnection();

// 路由分发,通过 Request 对象示例获取路径信息进行匹配
if ($request->getPath() == '/') {
    $albums = $connection->table('albums')->selectAll();
    include __DIR__  . "/../views/home.php";
} elseif ($request->getPath() == 'album') {
    $id = intval($request->get('id'));
    if (empty($id)) {
        echo '请指定要访问的专辑 ID';
        exit();
    }
    $album = $connection->table('albums')->select($id);
    $posts = $connection->table('posts')->selectByWhere(['album_id' => $id]);
    include __DIR__  . '/../views/album.php';
} elseif ($request->getPath() == 'post') {
    $id = intval($request->get('id'));
    if (empty($id)) {
        echo '请指定要访问的文章 ID';
        exit();
    }
    $post = $connection->table('posts')->select($id);
    $printer = $container->resolve(\App\Printer\PrinterContract::class);
    if ($container->resolve('app.editor') == 'markdown') {
        $post['content'] = $printer->driver('markdown')->render($post['text']);
    } else {
        $post['content'] = $printer->render($post['html']);
    }
    $pageTitle = $post['title'] . ' - ' . $container->resolve('app.name');
    $album = $connection->table('albums')->select($post['album_id']);
    include __DIR__  . '/../views/post.php';
} else {
    // 改为通过 Response 对象发送重定向响应
    $response = new \App\Http\Response('', 301, ['Location' => '/']);
    $response->prepare($request)->send();
}

由于我们基于 Composer 来管理命名空间和类的自动加载,所以在起始行引入了 vendor/autoload.php,关于其原理,上篇教程已经介绍过,接下来,我们引入调整路径后的 bootstrap.php 初始化应用,然后调用 Request 类的静态方法 capture 捕获并初始化全局请求实例 $request

在路由分发代码中,可以看到,之前的 $_GET$_SERVER 超全局变量已经不见踪影,取而代之的,我们通过调用 $request 实例上的 getPath 方法获取请求路径信息,作为路由分发的依据,在获取请求参数时,也调整为了调用 $request->get() 方法,然后传入参数名作为键,该方法可以获取所有请求参数,包括 GET 请求和 POST 请求的(换言之,就是查询字符串和请求实体中的参数)。

最后,在兜底逻辑中,我们基于 Response 对象设置响应状态码和响应头,对于 Response 类的构造函数,第一个参数是响应实体(默认是空字符串,这里是重定向响应,故而留空),第二个参数是响应状态码(默认是 200,这里是重定向响应,故而设置为 301),第三个参数是响应头(以关联数组方式支持传入多个响应头,默认是空数组,这里,我们设置 Location 作为重定向的跳转路径):

public function __construct(?string $content = '', int $status = 200, array $headers = [])

初始化响应对象后,通过 prepare 方法基于请求对象设置响应头,然后调用 send 方法将响应发送给客户端。对于视图响应,需要引入更复杂的逻辑来实现,所以保留之前的代码不做更改。

下篇教程,我们将基于封装好的 RequestResponse 对象编写基本的 HTTP 路由器实现。

PS:实际上,使用 Symfony HTTP Foundation 组件封装请求响应类的 PHP 项目非常多,包括大名鼎鼎的 Laravel、Drupal、Joomla! 等:

-w557

上一篇: PHP 命名空间与类自动加载实现

下一篇: 通过 PHP 原生代码实现 HTTP 路由器