理解 HTTP 消息和 PSR
超文本传输协议 (HTTP) 是客户端计算机和 Web 服务器之间的通信协议。诸如 Chrome、Safari 或 Firefox 等 Web 浏览器可以是 Web 客户端或用户代理,而侦听某个端口的计算机上的 Web 应用程序可以是 Web 服务器。Web 客户端不仅是浏览器,任何可以与 Web 服务器通信的应用程序(如 cURL 或 Telnet)都可以是 Web 客户端。
客户端通过互联网打开连接,向服务器发出请求,并等待接收服务器的响应。请求包含请求信息,而响应包含状态信息和请求的内容。这两种交换的数据称为 HTTP 消息。它们只是以 ASCII 编码的文本主体,并以以下结构跨越多行:
Start-line
HTTP Headers
Body
这看起来非常简单明了,不是吗?虽然可能是这样,但让我们详细说明一下这种结构:
-
起始行 描述了实现的请求方法(例如 GET、PUT 或 POST)、请求目标(通常是 URI)以及 HTTP 版本或响应的状态(例如 200、404 或 500)和 HTTP 版本。起始行始终是单行。
-
HTTP 标头行描述了请求或响应的特定详细信息(元信息),例如 Host、User-Agent、Server、Content-type 等。
-
空行 表示已发送请求的所有元信息。
-
消息体 包含请求(例如 HTML 表单的内容)或响应(例如 HTML 文档的内容)的交换数据。消息体是可选的(有时,在从服务器请求数据的请求中不需要消息体)。
现在,让我们使用 cURL
来查看 HTTP 请求和响应的数据是如何交换的:
-
使用内置的 PHP Web 服务器在 localhost:8181 上提供您在上一节中了解到的 PHP "Hello World" 应用程序:
$ php -S localhost:8181 -t public
-
在终端上打开一个新标签页,然后运行以下
cURL
脚本:$ curl http://0.0.0.0:8181 \ --trace-ascii \ /dev/stdout
您应该看到请求消息显示在第一部分,如下所示:
== Info: Trying 0.0.0.0:8181...
== Info: TCP_NODELAY set
== Info: Connected to 0.0.0.0 (127.0.0.1) port 8181 (0)
=> Send header, 76 bytes (0x4c)
0000: GET / HTTP/1.1
0010: Host: 0.0.0.0:8181
0024: User-Agent: curl/7.65.3
003d: Accept: /
004a:
在这里,您可以看到空行在 004a 处表示,并且请求中根本没有消息体。响应消息显示在第二部分,如下所示:
== Info: Mark bundle as not supporting multiuse
<= Recv header, 17 bytes (0x11)
0000: HTTP/1.1 200 OK
<= Recv header, 20 bytes (0x14)
0000: Host: 0.0.0.0:8181
<= Recv header, 37 bytes (0x25)
0000: Date: Sat, 21 Mar 2020 20:33:09 GMT
<= Recv header, 19 bytes (0x13)
0000: Connection: close
<= Recv header, 25 bytes (0x19)
0000: X-Powered-By: PHP/7.4.4
<= Recv header, 40 bytes (0x28)
0000: Content-type: text/html; charset=UTF-8
<= Recv header, 2 bytes (0x2)
0000:
<= Recv data, 12 bytes (0xc)
0000: Hello world!
== Info: Closing connection 0
在这里,您可以看到响应的起始行中的状态是 200 OK。但是在前面的示例中,我们没有发送任何数据,因此请求消息中没有消息体。让我们创建另一个非常基本的 PHP
脚本,如下所示:
-
创建一个带有 PHP
print_r
函数的 PHP 页面,以便显示POST
数据,如下所示:// public/index.php <?php print_r($_POST);
-
使用内置的 PHP Web 服务器在 localhost:8181 上提供该页面:
$ php -S localhost:8181 -t public
-
通过终端上的
cURL
发送一些数据:$ curl http://0.0.0.0:8181 \ -d "param1=value1¶m2=value2" \ --trace-ascii \ /dev/stdout
这一次,请求消息将显示在第一部分,以及消息体:
== Info: Trying 0.0.0.0:8181... == Info: TCP_NODELAY set == Info: Connected to 0.0.0.0 (127.0.0.1) port 8181 (0) => Send header, 146 bytes (0x92) 0000: POST / HTTP/1.1 0011: Host: 0.0.0.0:8181 0025: User-Agent: curl/7.65.3 003e: Accept: / 004b: Content-Length: 27 005f: Content-Type: application/x-www-form-urlencoded 0090: => Send data, 27 bytes (0x1b) 0000: param1=value1¶m2=value2 == Info: upload completely sent off: 27 out of 27 bytes
响应消息显示在第二部分,如下所示:
== Info: Mark bundle as not supporting multiuse <= Recv header, 17 bytes (0x11) 0000: HTTP/1.1 200 OK <= Recv header, 20 bytes (0x14) 0000: Host: 0.0.0.0:8181 <= Recv header, 37 bytes (0x25) 0000: Date: Sat, 21 Mar 2020 20:43:06 GMT <= Recv header, 19 bytes (0x13) 0000: Connection: close <= Recv header, 25 bytes (0x19) 0000: X-Powered-By: PHP/7.4.4 <= Recv header, 40 bytes (0x28) 0000: Content-type: text/html; charset=UTF-8 <= Recv header, 2 bytes (0x2) 0000: <= Recv data, 56 bytes (0x38) 0000: Array.(. [param1] => value1. [param2] => value2.). Array ( [param1] => value1 [param2] => value2 ) == Info: Closing connection 0
-
在这里,您还可以在终端上看到使用
cURL
发送带有PUT
方法的请求消息和请求消息:$ curl -X PUT http://0.0.0.0:8181 \ -d "param1=value1¶m2=value2" \ --trace-ascii \ /dev/stdout
-
同样适用于使用
cURL
发送带有DELETE
方法的请求,如下所示:$ curl -X DELETE http://0.0.0.0:8181 \ -d "param1=value1¶m2=value2" \ --trace-ascii \ /dev/stdout
-
最后但并非最不重要的一点是,我们还可以使用 Google Chrome 中的开发者工具来帮助我们检查交换的数据。让我们创建另一个简单的 PHP 脚本,该脚本将接收来自 URI 的数据:
// public/index.php <?php print_r($_GET);
-
通过使用
0.0.0.0:8181/?param1=value1¶m2=value2
在浏览器上发送一些数据。通过这样做,数据将作为param1=value1¶m2=value2
发送,如下面的屏幕截图所示:
如果你想了解更多关于 HTTP 和 HTTP 消息的信息,请访问 https://developer.mozilla.org/zh-CN/docs/Web/HTTP 以获取关于 HTTP 的一般信息,以及 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Messages 以获取关于 HTTP 消息的特定信息。 |
当涉及到服务器端开发时,HTTP 消息最好封装在对象中,这样更易于操作。例如,Node.js 有一个内置的 HTTP 模块 (https://nodejs.dev/the-nodejs-http-module) 用于 HTTP 通信,在使用 http.createServer()
方法创建 HTTP 服务器时,您可以从回调中获取 HTTP 消息对象:
const http = require('http')
http.createServer((request, response) => {
response.writeHead(200, { 'Content-Type': 'text/plain' })
response.end('Hello World')
}).listen(8080)
如果您使用的是像 Koa
这样的 Node.js 框架,您可以在 ctx
中找到 HTTP 消息对象,如下所示:
const Koa = require('koa')
const app = new Koa()
app.use(async ctx => {
ctx
ctx.request
ctx.response
})
在上面的代码中,ctx
是 Koa 的上下文,而 ctx.request
是 HTTP 请求消息,ctx.response
是 HTTP 响应消息。我们可以在 Express
中做同样的事情;您可以找到如下所示的 HTTP 消息:
const express = require('express')
const app = express()
app.get('/', (req, res) => res.send('Hello World!'))
与 Node.js 不同,PHP 从来没有像这样的内置 HTTP 消息对象。有很多方法可以手动和直接地获取和设置 Web
数据,就像我们在前面的 PHP 示例中看到的那样,通过使用超全局变量($_GET
、$_POST
)和内置函数(echo
、print_r
)。如果您想捕获传入的请求,您可以根据具体情况使用 $_GET
、$_POST
、$_FILE
、$_COOKIE
、$_SESSION
或任何其他超全局变量 ( https://www.php.net/manual/en/language.variables.superglobals.php )。
返回响应也是如此:您使用诸如 echo
、print
和 header
等全局函数来手动设置响应头。过去,PHP 开发者和框架都有自己实现 HTTP 消息的方式。这导致了一段时间内不同的框架有不同的抽象来表示 HTTP 消息,并且任何基于特定 HTTP 消息实现的应用程序都很难在项目中使用其他框架进行互操作。这种缺乏行业标准使得框架的组件紧密耦合。如果您不是从一个框架开始,您最终会自己构建一个。
但是今天,PHP 社区已经学习并强制执行 PHP 标准和建议。您不必完全遵守这些标准和建议;如果您有哲学上的理由促使您这样做,您可以忽略它们。但它们是一种善意的措施,旨在结束 PHP 的争论——至少在商业和协作方面是这样。并且一劳永逸地,PHP 开发者可以以与框架无关的方式专注于 PHP 标准而不是框架。当我们谈论 PHP 标准时,我们倾向于指的是 PSR,这是由 PHP 框架互操作组 (PHP-FIG) 定义和发布的 PHP 规范。PSR-7:HTTP 消息接口是 PHP-FIG 成员提出的规范之一,并根据他们商定的既定协议进行了投票。
PSR-7 于 2015 年 5 月正式通过。它主要用于标准化 HTTP 消息接口。在深入了解 PSR-7 之前,我们还应该了解一些其他的 PSR 编号,特别是 PSR-12(PSR-2 的替代)、PSR-4 和 PSR-15。本书将引导您了解它们,以便您可以编写可重用的、与框架无关的应用程序和组件,这些应用程序和组件可以独立使用,也可以与其他框架(无论是全栈框架还是微框架)互操作。让我们开始吧!
为什么需要 PSR?
在内部,PHP 从不告诉开发者应该如何编写 PHP 代码。例如,Python 使用缩进来表示代码块,而对于其他编程语言(如 PHP 和 JavaScript),代码中的缩进是为了提高可读性。以下是 Python 可以接受的示例:
age = 20
if age == 20:
print("age is 20")
如果没有缩进,Python
将返回错误:
if age == 20:
print("age is 20")
空格的数量取决于编码者的偏好,但您必须至少使用一个空格,并且同一代码块中的其他行必须具有相同数量的空格;否则,Python
将返回错误:
if age == 20:
print("age is 20")
print("age is 20")
另一方面,在 PHP
中,您可以编写以下代码:
if (age == 20) {
print("age is 20");
}
以下在 PHP
中也是有效的:
if (age == 20) {
print("age is 20");
print("age is 20");
}
Python
在内部强制执行代码的可读性和整洁性。PHP 则不然。您可以想象,如果没有一些基本的强制措施,并且取决于编码者的经验,PHP 代码最终可能会变得非常混乱、难看且难以阅读。也许 PHP Web 开发的低门槛在其中发挥了一定的作用。因此,您的 PHP 代码必须遵守通用的代码风格,以便于协作和维护。
有一些针对特定框架的 PHP 编码标准,但它们或多或少基于(或类似于)PSR 标准:
|
务实地讲,您的代码应该遵守您所依赖的框架以及该特定框架的编码标准。但是,如果您只使用框架中的某些组件或库,那么您可以遵守 PSR 的任意组合,或者 PEAR 制定的编码标准。PEAR 编码标准可以在 https://pear.php.net/manual/en/standards.php 找到。
本书侧重于各种 PSR,因为本章旨在创建与框架无关的 PHP 应用程序。您不一定同意 PSR,但如果您正在寻找一个启动项目的标准,并且您的组织内部没有任何自己的标准,那么 PSR 可能是一个不错的起点。您可以在 https://www.php-fig.org/psr/ 找到更多关于 PSR 的信息。
除了我们在这里提到的内容之外,您还应该查看 PHP: The Right Way。它概述了现代 PHP 编码人员可以作为参考的内容,从 PHP 的设置、使用 Composer 进行依赖管理(我们将在本章稍后介绍)、编码风格指南(其中推荐使用 PSR)、依赖注入、数据库、模板到测试框架等等。对于想要避免过去错误并在 Web 上查找权威 PHP 教程链接的新 PHP 编码人员来说,这是一个很好的起点。对于需要快速参考和来自广大 PHP 社区的更新,或者他们过去几年可能错过任何内容的经验丰富的 PHP 编码人员来说,这也是一个很好的资源。
现在,让我们深入了解 PSR,从 PSR-12 开始。
PSR-12 – 扩展编码风格指南
PSR-12 是 PSR-2 的修订版编码风格指南,它考虑了 PHP 7。PSR-12 规范于 2019 年 8 月 9 日获得批准。由于 PSR-2 于 2012 年被接受,PHP 发生了许多变化,这些变化对编码风格指南产生了一些影响,其中最值得注意的是 PHP 7 中引入的返回类型声明,PSR-2 中并未对其进行描述。因此,应该为使用它们定义一个标准,以便在各个 PHP 编码人员实施其可能最终会相互冲突的标准之前,更广泛的 PHP 社区可以采纳它们。
例如,PHP 7 中添加的返回类型声明只是指定了函数应该返回的值的类型。让我们看一下以下采用返回类型声明的函数:
declare(strict_types = 1);
function returnInt(int $value): int
{
return $value;
}
print(returnInt(2));
您将得到正确的整数结果 2。但是,如果我们更改 returnInt
函数内部的代码,如下所示,会发生什么呢?
function returnInt(int $value): int
{
return $value + 1.0;
}
PHP 将报错如下:
PHP Fatal error: Uncaught TypeError: Return value of returnInt() must be of
the type int, float returned in ...
因此,为了适应 PHP 7 的这一新特性,PSR-12 要求在冒号后使用一个空格,然后是带有返回类型声明的方法的类型声明。此外,冒号和声明必须与参数列表的右括号在同一行,并且两者之间没有空格。让我们看一个带有返回类型声明的简单示例:
class Fruit
{
public function setName(int $arg1, $arg2): string
{
return 'kiwi';
}
}
PSR-2 和 PSR-12 中保留了一些相同的规则。例如,在两个 PSR 中,您都不能使用制表符进行缩进,而必须使用四个单独的空格。但是,PSR-2 中关于块列表的规则已进行了修订。现在,在 PSR-12 中,即使只有一个导入,使用语句导入类、函数和常量的块也必须用一个空行分隔。让我们快速看一下符合此规则的一些代码:
<?php
/**
* The block of comments...
*/
declare(strict_types=1);
namespace VendorName\PackageName;
use VendorName\PackageName\{ClassX as X, ClassY, ClassZ as Z};
use VendorName\PackageName\SomeNamespace\ClassW as W;
use function VendorName\PackageName\{functionX, functionY, functionZ};
use const VendorName\PackageName\{ConstantX, ConstantY, ConstantZ};
/**
* The block of comments...
*/
class Fruit
{
//...
}
好的,明白了。以下是您提供的文本的原样简体中文翻译:
现在,你应该注意到,在 PSR-12 中,你必须在起始 <?php 标签后使用一个空行。然而,在 PSR-2 中,这不是必需的。例如,你可以这样写:
<?php
namespace VendorName\PackageName;
use FruitClass;
use VegetableClass as Veg;
值得了解的是,PSR-2 是从 PSR-1 扩展而来的,PSR-1 是一个基本的代码规范,但是自从 PSR-12 被接受以来,PSR-2 现在已被正式弃用。
要为你的代码实现这些 PSR,请访问以下站点:
如果你想了解 PHP 7 中的新特性,例如标量类型声明和返回类型声明,请访问 https://www.php.net/manual/en/migration70.new-features.php 。 |
PSR-12 帮助 PHP 编码人员编写更具可读性和结构化的代码,因此在 PHP 中编写代码时值得采纳它。现在,让我们继续讨论 PSR-4,它允许我们在 PHP 中使用自动加载。
PSR-4 – 自动加载器
在 PHP 的早期,如果您想将第三方库引入您的 PHP 项目,或者从单独的 PHP 文件引入您的函数和类,您会使用 include 或 require 语句。随着 PHP 自动加载的出现,您可以使用 __autoload 魔术方法(自 PHP 7.2 起已弃用)或 spl_autoload 来自动调用您的代码。然后,PHP 5.3 中出现了真正的命名空间支持,开发者和框架可以设计他们的方法来防止命名冲突。但是,由于不同方法之间的竞争,这仍然远非理想。您可以想象这样一种情况:您有两个框架——框架 A 和框架 B——而各个开发者彼此意见不合,并实施他们自己的方法来实现相同的结果。这简直是疯狂。
今天,我们遵守 PSR-4(它是 PSR-0 的后继者)来标准化自动加载方法,并将开发者和框架联系在一起。它规定了从文件路径自动加载类的标准。它还描述了文件的位置。因此,一个完全限定的类名应该遵循以下形式:
\<NamespaceName>(\<SubNamespaceNames>)\<ClassName>
在这个规则中,我们有以下内容:
-
完全限定的类的命名空间必须有一个顶级供应商命名空间,这是前面代码中的 <命名空间名称> 部分。
-
您可以使用一个或多个子命名空间,如前面代码的 <子命名空间名称> 部分所示。
-
然后,您必须以您的类名结束命名空间,如前面代码的 <类名> 部分所示。
因此,如果您正在编写自动加载器,建议使用此标准。但是,您不必(可能也不应该)费力地编写自己的符合 PSR-4 的自动加载器。这是因为您可以使用 Composer 来帮助您完成此操作。Composer 是 PHP 的包管理器。它类似于 Node.js 中的 npm。它最初于 2012 年发布。从那时起,所有现代 PHP 框架和 PHP 编码人员都使用它。这意味着您可以更多地专注于您的代码开发,而更少担心您将引入到项目环境中的不同包和库的互操作性。
在开始之前,请确保您的系统上已安装
|
当前版本是 1.10.9。请按照以下步骤安装 Composer
并利用它提供的自动加载器:
-
通过在终端中运行以下脚本,在当前目录中安装
Composer
:$ php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" $ php -r "if (hash_file('sha384', 'composer-setup.php') === 'e5325b19b381bfd88ce90a5ddb7823406b2a38cff6bb704b0acc289a09c8128d4a8ce2bbafcd1fcbdc38666422fe2806') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
-
运行
Composer
安装文件,如下所示:$ sudo php composer-setup.php
您应该在终端中看到以下输出:
All settings correct for using Composer Downloading... Composer (version 1.10.9) successfully installed to: /home/lau/composer.phar Use it: php composer.phar
-
删除
Composer
安装文件,如下所示:$ php -r "unlink('composer-setup.php');"
-
通过在终端上运行
php composer.phar
来验证安装。如果您想全局使用Composer
,则将Composer
移动到/usr/local/bin
(如果您使用的是Linux/Unix
):$ sudo mv composer.phar /usr/local/bin/composer
-
现在,您可以全局运行
Composer
。要验证它,只需运行以下命令:$ composer
您应该看到
Composer
的徽标,以及其可用的命令和选项:______ / ____/___ ____ ___ ____ ____ ________ _____ / / / __ \/ __ __ \/ __ \/ __ \/ ___/ _ \/ ___/ / /___/ /_/ / / / / / / /_/ / /_/ (__ ) __/ / \____/\____/_/ /_/ /_/ .___/\____/____/\___/_/ /_/ Composer version 1.10.9 2020-07-16 12:57:00 ... ...
或者,您可以使用
-V
选项直接检查您安装的版本:$ composer -V Composer version 1.10.9 2020-07-16 12:57:00
-
现在您的系统上已经安装了
Composer
,只需通过终端导航到您的项目根目录,然后使用composer require
,后跟<package-name>
,来安装您的项目中需要的任何第三方包(也称为依赖项),如下所示:$ composer require monolog/monolog
-
安装所需的包后,您可以转到您的项目根目录。您应该看到已创建一个
composer.json
文件,该文件在require
键中包含您项目的依赖项:{ "require": { "monolog/monolog": "^2.0" } }
-
如果你下次想再次安装所有依赖,你只需运行
install
命令,如下所示:$ composer install
-
当你安装了项目的依赖,无论是使用
require
还是install
命令,Composer
总会生成一个包含所有依赖的/vendor/
文件夹。一个autoload.php
文件也总会在/vendor/
文件夹内生成。然后你可以包含这个文件,并立即开始使用这些软件包提供的类,如下所示:<?php require __DIR__ . '/vendor/autoload.php'; $log = new Monolog\Logger('name'); $log->pushHandler(new Monolog\Handler\StreamHandler('path/to/your.log', Monolog\Logger::WARNING)); $log->addWarning('Foo'); $log->error('Bar');
-
最重要的是,你甚至可以通过将
autoload
键以及你的自定义命名空间添加到composer.json
文件中,来将你自己的类添加到自动加载器中。例如,你可以将你的类存储在项目根目录下的/src/
文件夹中,与/vendor/
目录位于同一级别:
{
"autoload": {
"psr-4": {
"Spectre\\": "src/"
}
}
}
如果你的源文件位于多个位置,你可以使用数组 [] 将其与你的自定义命名空间关联起来,如下所示:
{
"autoload": {
"psr-4": {
"Spectre\\": ["module1/", "module2/"]
}
}
}
Composer
将会为 Spectre
命名空间注册一个 PSR-4 自动加载器。之后,你就可以开始编写你的类了。例如,你可以创建一个包含 Spectre\Foo
类的 /src/Foo.php
文件。在那之后,只需在你的终端运行 dump-autoload
来重新生成 /vendor/
目录下的 autoload.php
文件。你也可以向 autoload
字段添加多个自定义命名空间,如下所示:
{
"autoload": {
"psr-4": {
"Spectre\\": [
//...
],
"AnotherNamespace\\": [
//...
]
}
}
}
除了 PSR-4,Composer 还支持 PSR-0。你可以向 composer.json 文件添加一个 psr-0 键。
有关如何将 PSR-0 与 Composer 一起使用的更多信息和示例,请访问 https://www.google.com/search?q=https://getcomposer.org/doc/04-schema.md%23autoload。然而,请注意 PSR-0 现在已被弃用。如果你想阅读更多关于这两个 PSR 的信息,请访问 https://www.php-fig.org/psr/psr-0/ 了解 PSR-0(已弃用)以及 https://www.php-fig.org/psr/psr-4/ 了解 PSR-4。 如果你想了解我们在前面的 PHP 日志记录示例中使用的 Monolog,请访问 https://github.com/Seldaek/monolog。如果你想阅读更多关于 PHP 中的类自动加载的信息,请访问 https://www.php.net/manual/en/language.oop5.autoload.php。 |
一旦你掌握了关于 PSR-12 和 PSR-4 的知识,你将更容易构建符合其他 PSR 的 PHP 应用程序。本书重点介绍的其他两个 PSR 是 PSR-7 和 PSR-15。让我们继续先看看 PSR-7。
PSR-7 – HTTP 消息接口
之前我们提到过,PHP 没有 HTTP 请求和响应消息对象,这就是为什么 PHP 框架和编码人员过去提出了不同的抽象来表示(或“模仿”)HTTP 消息。幸运的是,在 2015 年,PSR-7 的出现结束了这些“分歧”和差异。
PSR-7 是一组通用接口(抽象),当通过 HTTP 通信时,它为 HTTP 消息和 URI 指定了公共方法。在面向对象编程 (OOP) 中,接口实际上是对对象(类)必须实现的操作(公共方法)的抽象,而不定义这些操作是如何实现的复杂性和细节。例如,下表显示了当您组合 HTTP 消息类以使其符合 PSR-7 规范时,这些类必须实现的方法。
用于访问和修改请求和响应对象的指定方法如下:
获取方法 | 设置方法 |
---|---|
getProtocolVersion() |
withProtocolVersion($version) |
getHeaders() |
withHeader($name, $value) |
hasHeader($name) |
withAddedHeader($name, $value) |
getHeader($name) getHeaderLine($name) |
withoutHeader($name) |
getBody() |
withBody(StreamInterface $body) |
以下是仅用于访问和修改请求对象的指定方法:
获取方法 | 设置方法 |
---|---|
|
|
以下是仅用于访问和修改响应对象的指定方法:
获取方法 | 设置方法 |
---|---|
|
|
自从 PSR-7 于 2015 年 5 月 18 日被接受以来,已经涌现了许多基于它的软件包。只要您实现了 PSR-7 中指定的接口和方法,您就可以开发自己的版本。但是,除非您有充分的理由这样做,否则您可能是在“重新发明轮子”,因为已经存在一些 PSR-7 HTTP 消息软件包。因此,为了快速入门,让我们使用 Zend Framework 的 zend-diactoros。我们将“重用”您在前面章节中获得的 PSR 知识(PSR-12 和 PSR-4),来创建一个带有 HTTP 消息的简单“Hello World”服务器端应用程序。让我们开始吧:
-
在应用程序根目录中创建一个
/public/
目录,并在其中创建一个index.php
文件。将以下行添加到该文件中以引导应用程序环境:// public/index.php chdir(dirname(__DIR__)); require_once 'vendor/autoload.php';
在这两行代码中,我们将当前目录从
/path/to/public
更改为/path/to
,这样我们就可以通过编写vendor/autoload.php
而不是../vendor/autoload.php
来导入autoload.php
文件。__DIR__
(魔术常量)用于获取当前文件的目录路径,在本例中是/path/to/public/
目录中的index.php
。然后使用dirname
函数获取父目录的路径,即/path/to
。然后使用
chdir
函数更改当前目录。请注意,在接下来的关于 PSR 的章节中,我们将使用这种模式来引导应用程序环境并导入自动加载文件。请访问以下链接以了解更多关于前面提到的常量和函数的信息:
-
DIR(魔术常量):https://www.php.net/manual/en/language.constants.predefined.php
-
dirname 函数:https://www.php.net/manual/en/function.dirname.php
-
chdir 函数:https://www.php.net/manual/en/function.chdir.php
另请注意,您必须使用内置的 PHP Web 服务器在终端上运行所有传入的 PHP 应用程序,如下所示:
$ php -S localhost:8181 -t public
-
-
通过
Composer
将zend-diactoros
安装到应用程序的根目录中:$ composer require zendframework/zend-diactoros
-
要编组传入的请求,您应该在
/public/
目录的index.php
文件中创建一个请求对象,如下所示:$request = Zend\Diactoros\ServerRequestFactory::fromGlobals( $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES );
-
现在,我们可以创建一个响应对象并操作响应,如下所示:
$response = new Zend\Diactoros\Response(); $response->getBody()->write("Hello ");
-
请注意,write 方法是在流接口 (StreamInterface) 中指定的,并且我们还可以通过多次调用此方法来追加更多数据:
$response->getBody()->write("World!");
-
然后,如果需要,我们可以操作标头:
$response = $response ->withHeader('Content-Type', 'text/plain');
-
请注意,标头应该在数据写入正文后添加。就这样——您已经成功地将本章开头了解到的简单 PHP "Hello World" 应用程序转换为使用 PSR-7 的现代 PHP 应用程序!但是,如果您通过终端使用 php -S localhost:8181 -t public 在浏览器上运行此 PSR-7 "Hello World" 应用程序,您将在屏幕上看不到任何内容。这是因为我们没有使用 PSR-15 HTTP 服务器请求处理程序和 PSR-7 HTTP 响应发送器将响应发送到浏览器,我们将在下一节中介绍这些内容。如果您现在想查看输出,可以使用 getBody 方法访问数据,然后使用 echo 输出:
echo $response->getBody();
-
如果您通过 Chrome 上的开发者工具检查页面的 Content-type,您将得到 text/html 而不是我们使用 withHeader 方法修改的 text/plain。我们将在下一章中使用发送器获得正确的内容类型。
有关 zend-diactoros 及其高级用法的更多信息,请访问 https://docs.zendframework.com/zend-diactoros/。除了 Zend Framework 的 zend-diactoros 之外,您还可以使用来自其他框架和库的 HTTP 消息包:
-
Guzzle 和 PSR-7 (来自 Guzzle):http://docs.guzzlephp.org/en/latest/psr7.html
-
HTTPlug (来自 PHP-HTTP):http://docs.php-http.org/en/latest/
-
PSR-7 Bridge (来自 Symfony):https://symfony.com/doc/master/components/http_foundation.html
-
Slim:http://www.slimframework.com
有关此 PSR 的更多信息,您应该查看 PSR-7 文档:https://www.php-fig.org/psr/psr-7/。如果您是 PHP 接口的新手,请访问 https://www.php.net/manual/en/language.oop5.interfaces.php 以进行进一步阅读。
-
从 PSR-7 文档中,您可以找到本书中未提及的其余公共方法。这些方法应该在任何 PSR-7 HTTP 消息包(如 zend-diactoros)中都可以找到。了解这些方法很有用,这样您就知道可以使用它们做什么。您还可以在运行时使用内置的 PHP get_class_methods 方法列出您可以在请求和响应对象中使用的所有方法。例如,对于请求对象,您可以执行以下操作:
$request = Zend\Diactoros\ServerRequestFactory::fromGlobals(
//...
);
print_r(get_class_methods($request));
您将获得一个包含您可以调用的请求方法的数组列表。响应对象也是如此;通过执行以下操作,您将获得一个包含响应方法的数组列表:
$response = new Zend\Diactoros\Response();
print_r(get_class_methods($response));
现在,让我们继续看看 PSR-15,在那里我们将了解如何将响应发送到客户端(浏览器)。
PSR-15 – HTTP 服务器请求处理器(请求处理器)
PSR-7 是 PHP 社区迈出的伟大一步,但这只是实现目标的半程,这个目标是使 PHP 编码人员摆脱单体 MVC 框架,并允许他们使用一系列可重用的中间件来构建与框架无关的 PHP 应用程序。它只定义了 HTTP 消息(请求和响应);它从未定义之后如何处理它们。因此,我们需要一个请求处理程序来处理请求以生成响应。
与 PSR-7 类似,PSR-15 也是一组通用接口,但它们更进一步,并为请求处理程序(HTTP 服务器请求处理程序)和中间件(HTTP 服务器请求中间件)指定了标准。它于 2018 年 1 月 22 日被接受。我们将在下一节中介绍 HTTP 服务器请求中间件。现在,让我们了解 PSR-15 接口 RequestHandlerInterface 中的 HTTP 服务器请求处理程序:
// Psr\Http\Server\RequestHandlerInterface
namespace Psr\Http\Server;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
interface RequestHandlerInterface
{
public function handle(ServerRequestInterface $request) : ResponseInterface;
}
正如您所见,这是一个非常简单的接口。它只有一个指定的公共方法 handle,该方法只接受一个 PSR-7 HTTP 请求消息,并且必须返回一个 PSR-7 HTTP 响应消息。我们将使用 Zend Framework 的 zend-httphandlerrunner 组件,该组件实现了此接口,以提供我们可以用来发送 PSR-7 响应的实用程序。让我们将其连接到应用程序:
-
通过 Composer 安装 zend-httphandlerrunner:
$ composer require zendframework/zend-httphandlerrunner
-
一旦我们在项目环境中安装了它,我们就可以将之前创建的响应发送到浏览器,如下所示:
//... $response = $response->withHeader('Content-Type', 'text/plain'); (new Zend\HttpHandlerRunner\Emitter\SapiEmitter)->emit($response);
如果您再次通过 Chrome 上的开发者工具检查页面的 Content-Type,您将获得正确的内容类型,即 text/plain。
有关 zend-httphandlerrunner 的更多信息,请访问 https://docs.zendframework.com/zend-httphandlerrunner/ 。有关 PSR-15 的更多信息,请访问 https://www.php-fig.org/psr/psr-15/。 |
除了 zend-httphandlerrunner 之外,您还可以使用 Narrowspark 的 Http Response Emitter 来处理请求并发送响应。现在,让我们继续看看 PSR-15 的第二个接口:MiddlewareInterface。
PSR-15 – HTTP 服务器请求处理器(中间件)
PSR-15 中的中间件接口具有以下抽象:
// Psr\Http\Server\MiddlewareInterface
namespace Psr\Http\Server;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
interface MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
) : ResponseInterface;
}
同样,您可以看到这是一个非常简单的接口。它只有一个为中间件实现指定的公共方法 process。实现此接口的组件(中间件)将只接受一个 PSR-7 HTTP 请求消息和一个 PSR-15 HTTP 服务器请求处理程序,然后必须返回一个 PSR-7 HTTP 响应消息。
我们将使用 Zend Framework 的 zend-stratigility 组件,该组件实现了此接口,以允许我们在应用程序中创建 PSR-15 中间件。让我们学习如何将其连接到应用程序:
-
通过 Composer 安装 zend-stratigility:
$ composer require zendframework/zend-stratigility
-
一旦我们在项目环境中安装了它,我们将导入 middleware 函数和 MiddlewarePipe 类,如下所示:
use function Zend\Stratigility\middleware; $app = new Zend\Stratigility\MiddlewarePipe(); // 创建一个请求 $request = Zend\Diactoros\ServerRequestFactory::fromGlobals( //... );
-
然后,我们可以使用这个 middleware 函数创建三个中间件并将它们附加到管道,如下所示:
$app->pipe(middleware(function ($request, $handler) { $response = $handler->handle($request); return $response ->withHeader('Content-Type', 'text/plain'); })); $app->pipe(middleware(function ($request, $handler) { $response = $handler->handle($request); $response->getBody()->write("User Agent: " . $request->getHeader('user-agent')[0]); return $response; })); $app->pipe(middleware(function ($request, $handler) { $response = new Zend\Diactoros\Response(); $response->getBody()->write("Hello world!\n"); $response->getBody()->write("Request method: " . $request->getMethod() . "\n"); return $response; }));
-
正如您所见,我们之前创建的“Hello World”代码块现在是一个与其他中间件堆叠在一起的中间件。最后,我们可以从这些中间件生成最终响应并将其发送到浏览器,如下所示:
$response = $app->handle($request); (new Zend\HttpHandlerRunner\Emitter\SapiEmitter)-> emit($response);
您应该在浏览器上的 0.0.0.0:8181 看到类似于以下结果:
Hello world! Request method: GET User Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
有关 zend-stratigility 的更多信息,请访问 https://www.google.com/search?q=https://docs.zendframework.com/zend-stratigility/。
除了 zend-stratigility 之外,您还可以使用以下软件包创建您的中间件:
-
Northwoods Broker: https://github.com/northwoods/broker
-
Relay: https://relayphp.com/
-
就这样。借助几个可互操作的组件,我们引导了一个符合 PSR-12、PSR-7 和 PSR-15 的现代 PHP 应用程序,这意味着您可以自由地(与框架无关地)从广泛的供应商实现中选择这些标准的 HTTP 消息、请求处理程序和中间件。但我们还没有完成。您可能已经注意到,我们创建的应用程序只是一个在 0.0.0.0:8181 的单个“路由”上运行的单页应用程序。它没有任何其他路由,例如 /about、/contact 等。因此,我们需要一个实现 PSR-15 的路由器。我们将在下一节中介绍这一点。
PSR-7/PSR-15 路由器
我们将使用 The League of Extraordinary Packages(一个 PHP 开发者组织)的 Route,以便拥有一个 PSR-7 路由系统并在其上分发我们的 PSR-15 中间件。简而言之,Route 是一个快速的 PSR-7 路由/分发包。
它是一个 PSR-15 服务器请求处理程序,可以处理中间件堆栈的调用。它基于 Nikita Popov 的 FastRoute (https://github.com/nikic/FastRoute) 构建。
让我们学习如何将其连接到应用程序:
-
通过
Composer
安装league/route
:$ composer require league/route
-
安装完成后,我们可以使用路由重构我们的 “Hello World” 组件,如下所示:
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; $request = Zend\Diactoros\ServerRequestFactory::fromGlobals( //... ); $router = new League\Route\Router; $router->map('GET', '/', function (ServerRequestInterface $request) : ResponseInterface { $response = new Zend\Diactoros\Response; $response->getBody()->write('<h1>Hello, World!</h1>'); return $response; });
-
然后,我们只需要使用 Route 的 dispatch 方法创建一个 PSR-7 HTTP 响应并将其发送到浏览器:
$response = $router->dispatch($request); (new Zend\HttpHandlerRunner\Emitter\SapiEmitter)->emit($response);
查看您可以在 https://route.thephpleague.com/4.x/route 上使用的 HTTP 请求方法列表(get、post、put、delete 等)。更重要的是,我们可以将中间件附加到我们的应用程序。
-
如果您想锁定整个应用程序,可以将中间件添加到路由器,如下所示:
use function Zend\Stratigility\middleware; $router = new League\Route\Router; $router->middleware(<middleware>);
-
如果您想锁定一组路由,可以将中间件添加到该组,如下所示:
$router ->group('/private', function ($router) { // ... 添加路由 }) ->middleware(<middleware>) ;
-
如果您想锁定特定路由,可以将中间件添加到该路由,如下所示:
$router ->map('GET', '/secret', <SomeController>) ->middleware(<middleware>) ;
-
例如,您可以将
Route
与zend-stratigility
结合使用:use function Zend\Stratigility\middleware; $router = new League\Route\Router; $router->middleware(middleware(function ($request, $handler) { //... }));
-
如果您不想使用
middleware
函数或者根本不想使用zend-stratigility
,您可以创建匿名中间件,如下所示:use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; $router = new League\Route\Router; $router->middleware(new class implements MiddlewareInterface { public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface { $response = $handler->handle($request); return $response->withHeader('X-Clacks-Overhead', 'GNU Terry Pratchett'); } });
只要您通过在中间件中实现 process 方法来遵守 PSR-7 和 PSR-15,就完全不需要 zend-stratigility。如果您想在单独的 PHP 文件中创建基于类的中间件,请查看 https://route.thephpleague.com/4.x/middleware/ 提供的示例。
有关 The League of Extraordinary Packages 的 Route 的更多信息,请访问 https://route.thephpleague.com/。您还可以查看此开发者组创建的其他软件包:https://thephpleague.com/。除了 The League of Extraordinary 的 Route 之外,您还可以使用以下基于 PSR-7 和 PSR-15 的 HTTP 路由器软件包:
|
您可能需要一个分发器来与其中一些软件包一起使用。使用 The League of Extraordinary Packages 的 Route
的优势在于它在一个软件包中提供了一个路由器和一个分发器。
至此,我们已经使用 PSR-12、PSR-4、PSR-7 和 PSR-15 构建了一个与框架无关的 PHP 应用程序。但是我们的 PHP API 尚未完成。还有一项任务要做——我们需要添加一个用于 CRUD 操作的数据库框架。我们将在下一节中指导您完成此任务。