理解 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 请求和响应的数据是如何交换的:

  1. 使用内置的 PHP Web 服务器在 localhost:8181 上提供您在上一节中了解到的 PHP "Hello World" 应用程序:

    $ php -S localhost:8181 -t public
  2. 在终端上打开一个新标签页,然后运行以下 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 脚本,如下所示:

  1. 创建一个带有 PHP print_r 函数的 PHP 页面,以便显示 POST 数据,如下所示:

    // public/index.php
    <?php
    print_r($_POST);
  2. 使用内置的 PHP Web 服务器在 localhost:8181 上提供该页面:

    $ php -S localhost:8181 -t public
  3. 通过终端上的 cURL 发送一些数据:

    $ curl http://0.0.0.0:8181 \
    -d "param1=value1&param2=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&param2=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
  4. 在这里,您还可以在终端上看到使用 cURL 发送带有 PUT 方法的请求消息和请求消息:

    $ curl -X PUT http://0.0.0.0:8181 \
    -d "param1=value1&param2=value2" \
    --trace-ascii \
    /dev/stdout
  5. 同样适用于使用 cURL 发送带有 DELETE 方法的请求,如下所示:

    $ curl -X DELETE http://0.0.0.0:8181 \
    -d "param1=value1&param2=value2" \
    --trace-ascii \
    /dev/stdout
  6. 最后但并非最不重要的一点是,我们还可以使用 Google Chrome 中的开发者工具来帮助我们检查交换的数据。让我们创建另一个简单的 PHP 脚本,该脚本将接收来自 URI 的数据:

    // public/index.php
    <?php
    print_r($_GET);
  7. 通过使用 0.0.0.0:8181/?param1=value1&param2=value2 在浏览器上发送一些数据。通过这样做,数据将作为 param1=value1&param2=value2 发送,如下面的屏幕截图所示:

    image 2025 04 30 14 53 35 794

如果你想了解更多关于 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)和内置函数(echoprint_r)。如果您想捕获传入的请求,您可以根据具体情况使用 $_GET$_POST$_FILE$_COOKIE$_SESSION 或任何其他超全局变量 ( https://www.php.net/manual/en/language.variables.superglobals.php )。

返回响应也是如此:您使用诸如 echoprintheader 等全局函数来手动设置响应头。过去,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 标准:

  • Zend 编码标准:https://www.google.com/search?q=https://framework.zend.com/manual/2.4/en/ref/coding.standard.html

  • Symfony 编码标准:https://www.google.com/search?q=https://symfony.com/doc/master/contributing/code/standards.html

  • CakePHP 编码标准:https://book.cakephp.org/3.0/en/contributing/cakephp-coding-conventions.html

  • FuelPHP 编码标准:https://fuelphp.com/docs/general/coding_standards.html

  • WordPress 编码标准:https://codex.wordpress.org/WordPress_Coding_Standards

务实地讲,您的代码应该遵守您所依赖的框架以及该特定框架的编码标准。但是,如果您只使用框架中的某些组件或库,那么您可以遵守 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 编码人员都使用它。这意味着您可以更多地专注于您的代码开发,而更少担心您将引入到项目环境中的不同包和库的互操作性。

在开始之前,请确保您的系统上已安装 Composer。根据您的系统,您可以按照以下指南安装 Composer

  • 来自 Composer 官方网站:https://getcomposer.org/doc/00-intro.md 和 https://getcomposer.org/download/

  • 来自 PHP: The Right Way:https://www.google.com/search?q=https://phptherightway.com/dependency_management

当前版本是 1.10.9。请按照以下步骤安装 Composer 并利用它提供的自动加载器:

  1. 通过在终端中运行以下脚本,在当前目录中安装 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;"
  2. 运行 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
  3. 删除 Composer 安装文件,如下所示:

    $ php -r "unlink('composer-setup.php');"
  4. 通过在终端上运行 php composer.phar 来验证安装。如果您想全局使用 Composer,则将 Composer 移动到 /usr/local/bin(如果您使用的是 Linux/Unix):

    $ sudo mv composer.phar /usr/local/bin/composer
  5. 现在,您可以全局运行 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
  6. 现在您的系统上已经安装了 Composer,只需通过终端导航到您的项目根目录,然后使用 composer require,后跟 <package-name>,来安装您的项目中需要的任何第三方包(也称为依赖项),如下所示:

    $ composer require monolog/monolog
  7. 安装所需的包后,您可以转到您的项目根目录。您应该看到已创建一个 composer.json 文件,该文件在 require 键中包含您项目的依赖项:

    {
        "require": {
            "monolog/monolog": "^2.0"
        }
    }
  8. 如果你下次想再次安装所有依赖,你只需运行 install 命令,如下所示:

    $ composer install
  9. 当你安装了项目的依赖,无论是使用 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');
  10. 最重要的是,你甚至可以通过将 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)

以下是仅用于访问和修改请求对象的指定方法:

获取方法 设置方法
  • getRequestTarget()

  • getMethod()

  • getUri()

  • getServerParams()

  • getCookieParams()

  • getQueryParams()

  • getUploadedFiles()

  • getParsedBody()

  • getAttributes()

  • getAttribute($name,$default = null)

  • withMethod($method)

  • withRequestTarget($requestTarget)

  • withUri(UriInterface $uri, $preserveHost = false)

  • withCookieParams(array $cookies)

  • withQueryParams(array $query)

  • withUploadedFiles(array $uploadedFiles)

  • withParsedBody($data)

  • withAttribute($name, $value)

  • withoutAttribute($name)

以下是仅用于访问和修改响应对象的指定方法:

获取方法 设置方法
  • getStatusCode()

  • getReasonPhrase()

  • withStatus($code, $reasonPhrase = '')

自从 PSR-7 于 2015 年 5 月 18 日被接受以来,已经涌现了许多基于它的软件包。只要您实现了 PSR-7 中指定的接口和方法,您就可以开发自己的版本。但是,除非您有充分的理由这样做,否则您可能是在“重新发明轮子”,因为已经存在一些 PSR-7 HTTP 消息软件包。因此,为了快速入门,让我们使用 Zend Framework 的 zend-diactoros。我们将“重用”您在前面章节中获得的 PSR 知识(PSR-12 和 PSR-4),来创建一个带有 HTTP 消息的简单“Hello World”服务器端应用程序。让我们开始吧:

  1. 在应用程序根目录中创建一个 /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
  2. 通过 Composerzend-diactoros 安装到应用程序的根目录中:

    $ composer require zendframework/zend-diactoros
  3. 要编组传入的请求,您应该在 /public/ 目录的 index.php 文件中创建一个请求对象,如下所示:

    $request = Zend\Diactoros\ServerRequestFactory::fromGlobals(
        $_SERVER,
        $_GET,
        $_POST,
        $_COOKIE,
        $_FILES
    );
  4. 现在,我们可以创建一个响应对象并操作响应,如下所示:

    $response = new Zend\Diactoros\Response();
    $response->getBody()->write("Hello ");
  5. 请注意,write 方法是在流接口 (StreamInterface) 中指定的,并且我们还可以通过多次调用此方法来追加更多数据:

    $response->getBody()->write("World!");
  6. 然后,如果需要,我们可以操作标头:

    $response = $response
        ->withHeader('Content-Type', 'text/plain');
  7. 请注意,标头应该在数据写入正文后添加。就这样——您已经成功地将本章开头了解到的简单 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();
  8. 如果您通过 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 响应的实用程序。让我们将其连接到应用程序:

  1. 通过 Composer 安装 zend-httphandlerrunner:

    $ composer require zendframework/zend-httphandlerrunner
  2. 一旦我们在项目环境中安装了它,我们就可以将之前创建的响应发送到浏览器,如下所示:

    //...
    $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 中间件。让我们学习如何将其连接到应用程序:

  1. 通过 Composer 安装 zend-stratigility:

    $ composer require zendframework/zend-stratigility
  2. 一旦我们在项目环境中安装了它,我们将导入 middleware 函数和 MiddlewarePipe 类,如下所示:

    use function Zend\Stratigility\middleware;
    
    $app = new Zend\Stratigility\MiddlewarePipe();
    
    // 创建一个请求
    $request = Zend\Diactoros\ServerRequestFactory::fromGlobals(
        //...
    );
  3. 然后,我们可以使用这个 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;
    }));
  4. 正如您所见,我们之前创建的“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 之外,您还可以使用以下软件包创建您的中间件:

就这样。借助几个可互操作的组件,我们引导了一个符合 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) 构建。

让我们学习如何将其连接到应用程序:

  1. 通过 Composer 安装 league/route

    $ composer require league/route
  2. 安装完成后,我们可以使用路由重构我们的 “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;
    });
  3. 然后,我们只需要使用 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 等)。更重要的是,我们可以将中间件附加到我们的应用程序。

  4. 如果您想锁定整个应用程序,可以将中间件添加到路由器,如下所示:

    use function Zend\Stratigility\middleware;
    
    $router = new League\Route\Router;
    $router->middleware(<middleware>);
  5. 如果您想锁定一组路由,可以将中间件添加到该组,如下所示:

    $router
        ->group('/private', function ($router) {
            // ... 添加路由
        })
        ->middleware(<middleware>)
    ;
  6. 如果您想锁定特定路由,可以将中间件添加到该路由,如下所示:

    $router
        ->map('GET', '/secret', <SomeController>)
        ->middleware(<middleware>)
    ;
  7. 例如,您可以将 Routezend-stratigility 结合使用:

    use function Zend\Stratigility\middleware;
    
    $router = new League\Route\Router;
    $router->middleware(middleware(function ($request, $handler) {
        //...
    }));
  8. 如果您不想使用 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 路由器软件包:

  • delolmo/symfony-router:https://github.com/delolmo/symfony-router

  • middlewares/aura-router:https://github.com/middlewares/aura-router

  • middlewares/fast-route:https://github.com/middlewares/fast-route

  • timtegeler/routerunner:https://www.google.com/search?q=https://github.com/timtegeler/routerunner

  • sunrise-php/http-router:https://github.com/sunrise-php/http-router

您可能需要一个分发器来与其中一些软件包一起使用。使用 The League of Extraordinary Packages 的 Route 的优势在于它在一个软件包中提供了一个路由器和一个分发器。

至此,我们已经使用 PSR-12、PSR-4、PSR-7 和 PSR-15 构建了一个与框架无关的 PHP 应用程序。但是我们的 PHP API 尚未完成。还有一项任务要做——我们需要添加一个用于 CRUD 操作的数据库框架。我们将在下一节中指导您完成此任务。