基于 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、
要引入 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 AppHttp;
use SymfonyComponentHttpFoundationResponse 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 = AppHttpRequest::capture();
$store = $container->resolve(AppStoreStoreContract::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(AppPrinterPrinterContract::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 AppHttpResponse('', 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! 等:
- 分享微信小程序推送消息步骤
- 实例分享微信小程序项目搭建(下)
- 实例分享微信小程序项目搭建(上)
- Android6.0源码分析之蓝牙显示接收到的文件
- Android中应用调用系统权限
- Android5.0以后隐式启动ServiceBug
- Android6.0源码分析之录音功能(一)
- Android6.0源码开发之修改默认音量default及max和min
- Android源码开发之添加/删除系统应用
- 按键事件处理
- Android6.0锁屏源码分析之界面布局分析
- Android6.0源码分析之menu键弹出popupwindow菜单流程分析
- Android中初步自定义view
- Android中View研究自学之路 Android6.0源码分析之View(一)Android6.0源码分析之View(二)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- WAF原理及其使用说明
- 还在写Bug?GitHub官方代码扫描工具上线,免费查找漏洞
- n1.Docker安装运行所遇异常解决
- n3.Docker之Win10和Server使用实例
- GitHub 再见 Master !
- IT运维面试问题总结-Linux基础
- 6.Docker使用辅助工具汇总
- IT运维面试问题总结-基础服务、磁盘管理、虚拟平台和系统管理
- IT运维面试问题总结-运维工具、开源应用(Ansible、Ceph、Docker、Apache、Nginx等)
- IT运维面试问题总结-数据库、监控、网络管理(NoSQL、MongoDB、MySQL、Prometheus、Zabbix)
- IT运维面试问题总结-LVS、Keepalived、HAProxy、Kubernetes、OpenShift等
- GitHub 标星 119K+!这些神器仅需一行代码即可下载全网视频!
- React进阶(2)-上手实践Redux-如何获取store的数据
- 关于Python3.9,这张「新特性必知图」就够了
- 3.Docker学习之Dockerfile