基于 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
项目代码:
注:详细代码参见 https://github.com/nonfu/master-laravel-code/tree/v0.4/practice/blog。
我们将所有应用 PHP 代码都转移到了 app
目录下,并且为其设置了命名空间 App
,将对外公开的静态资源文件和入口文件 index.php
转移到了 public
目录,而将视图模板文件都转移到了 views
目录下。
基于 Symfony 基类封装请求响应类
注意到 app/http
这个子目录,我们将应用需要用到的 Request
、Response
、Session
类都放到这个目录下:
这三个类分别继承自 Symfony HTTP Foudation 组件的 Request
、Response
、Session
基类,这里,我们新增子类实现的目的是为了便于添加自定义逻辑。在 Request
子类中新增了两个方法,用于初始化 HTTP 请求和获取请求路径,而 Response
和 Session
目前没有定义任何新增方法:
<?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
方法将响应发送给客户端。对于视图响应,需要引入更复杂的逻辑来实现,所以保留之前的代码不做更改。
下篇教程,我们将基于封装好的 Request
和 Response
对象编写基本的 HTTP 路由器实现。
PS:实际上,使用 Symfony HTTP Foundation 组件封装请求响应类的 PHP 项目非常多,包括大名鼎鼎的 Laravel、Drupal、Joomla! 等:
8 条评论
厉害,
请问文章中讲的blog项目,在哪里讲过开发过程啊?在PHP 全栈工程师指南中也没找到。
作业里面
章节作业:基于面向对象编程重构博客系统,包含首页、列表页、详情页,要求引入依赖注入模式、单例模式、适配器模式和工厂模式。目前数据库基于数组驱动模拟实现。
能否增加过程讲解?感觉看到这里就断了。
这个是在学习辅导班里面的作业 这里就不单独介绍了 可以自己去看源码:https://github.com/nonfu/master-laravel-code
好的
作业在哪儿啊学院君
这里有说明:https://xueyuanjun.com/books/php-fullstack