十四、附录 A:定义 PSR-7 类

在本附录中,我们将介绍以下主题:

  • 实现 PSR-7 值对象类
  • 开发 PSR-7 请求类
  • 定义 PSR-7 响应类

导言

PHP 标准建议编号 7PSR-7定义了许多接口,但未提供实际实现。因此,我们需要定义具体的代码实现,以便开始创建定制中间件。

实现 PSR-7 值对象类

为了处理 PSR-7 请求和响应,我们首先需要定义一系列值对象。这些类表示基于 web 的活动中使用的逻辑对象,如 URI、文件上载和流式请求或响应体。

准备好了吗

PSR-7 接口的源代码以Composer包的形式提供。使用Composer管理外部软件(包括 PSR-7 接口)被认为是最佳实践。

怎么做。。。

  1. 首先,进入以下 URL 获取最新版本的 PSR-7 接口定义:https://github.com/php-fig/http-message 。还提供了源代码。在撰写本文时,以下定义可用:

    RequestInterface

    MessageInterface

    |

    界面

    |

    延伸

    |

    笔记

    |

    这些方法处理什么

    | | --- | --- | --- | --- | | MessageInterface | | 定义超文本传输协议消息通用的方法 | 头、消息体(即内容)和协议 | | RequestInterface | MessageInterface | 表示客户端生成的请求 | URI、HTTP 方法和请求目标 | | ServerRequestInterface | 表示从客户端到服务器的请求 | 服务器和查询参数、曲奇上载的文件和解析的正文 | | ResponseInterface | 表示从服务器到客户端的响应 | HTTP 状态代码和原因 | | StreamInterface | | 表示数据流 | 流行为,如查找、告知、读取、写入等 | | UriInterface | | 表示 URI | 方案(即 HTTP、HTTPS)主机、端口、用户名、密码(即(FTP)查询参数、路径和片段 | | UploadedFileInterface | | 处理上传的文件 | 文件大小、媒体类型、移动文件和文件名 |

  2. Unfortunately, we will need to create concrete classes that implement these interfaces in order to utilize PSR-7. Fortunately, the interface classes are extensively documented internally through a series of comments. We will start with a separate class that contains useful constants:

    提示

    请注意,我们利用了 PHP7 中引入的一个新特性,该特性允许我们将常量定义为数组。

    ```php namespace Application\MiddleWare; class Constants { const HEADER_HOST = 'Host'; // host header const HEADER_CONTENT_TYPE = 'Content-Type'; const HEADER_CONTENT_LENGTH = 'Content-Length';

    const METHOD_GET = 'get'; const METHOD_POST = 'post'; const METHOD_PUT = 'put'; const METHOD_DELETE = 'delete'; const HTTP_METHODS = ['get','put','post','delete'];

    const STANDARD_PORTS = [ 'ftp' => 21, 'ssh' => 22, 'http' => 80, 'https' => 443 ];

    const CONTENT_TYPE_FORM_ENCODED = 'application/x-www-form-urlencoded'; const CONTENT_TYPE_MULTI_FORM = 'multipart/form-data'; const CONTENT_TYPE_JSON = 'application/json'; const CONTENT_TYPE_HAL_JSON = 'application/hal+json';

    const DEFAULT_STATUS_CODE = 200; const DEFAULT_BODY_STREAM = 'php://input'; const DEFAULT_REQUEST_TARGET = '/';

    const MODE_READ = 'r'; const MODE_WRITE = 'w';

    // NOTE: not all error constants are shown to conserve space const ERROR_BAD = 'ERROR: '; const ERROR_UNKNOWN = 'ERROR: unknown';

    // NOTE: not all status codes are shown here! const STATUS_CODES = [ 200 => 'OK', 301 => 'Moved Permanently', 302 => 'Found', 401 => 'Unauthorized', 404 => 'Not Found', 405 => 'Method Not Allowed', 418 => 'I_m A Teapot', 500 => 'Internal Server Error', ]; } ```

    HTTP 状态码的完整列表可在此处找到:https://tools.ietf.org/html/rfc7231#section-6.1

  3. Next, we will tackle classes that represent value objects used by other PSR-7 classes. For a start, here is the class that represents a URI. In the constructor, we accept a URI string as an argument, and break it down into its component parts using the parse_url() function:

    ```php namespace Application\MiddleWare; use InvalidArgumentException; use Psr\Http\Message\UriInterface; class Uri implements UriInterface { protected $uriString; protected $uriParts = array();

    public function __construct($uriString) { $this->uriParts = parse_url($uriString); if (!$this->uriParts) { throw new InvalidArgumentException( Constants::ERROR_INVALID_URI); } $this->uriString = $uriString; } ```

    URI代表统一资源指标。这是您在提出请求时在浏览器顶部看到的内容。有关 URI 的更多信息,请参阅http://tools.ietf.org/html/rfc3986

  4. 在构造函数之后,我们定义了访问 URI 组件的方法。方案表示一个 PHP 包装器(即 HTTP、FTP 等):

    php public function getScheme() { return strtolower($this->uriParts['scheme']) ?? ''; }

  5. 权限表示用户名(如果存在)、主机以及可选的端口号:

    php public function getAuthority() { $val = ''; if (!empty($this->getUserInfo())) $val .= $this->getUserInfo() . '@'; $val .= $this->uriParts['host'] ?? ''; if (!empty($this->uriParts['port'])) $val .= ':' . $this->uriParts['port']; return $val; }

  6. 用户信息表示用户名(如果存在)和可选的密码。使用密码的示例是访问 FTP 网站时的,如ftp://username:password@website.com:/path

    php public function getUserInfo() { if (empty($this->uriParts['user'])) { return ''; } $val = $this->uriParts['user']; if (!empty($this->uriParts['pass'])) $val .= ':' . $this->uriParts['pass']; return $val; }

  7. 主机是 URI 中包含的 DNS 地址:

    php public function getHost() { if (empty($this->uriParts['host'])) { return ''; } return strtolower($this->uriParts['host']); }

  8. 端口是 HTTP 端口(如果存在)。您会注意到,如果我们的STANDARD_PORTS常量中列出了一个端口,则返回值为NULL,符合 PSR-7:

    php public function getPort() { if (empty($this->uriParts['port'])) { return NULL; } else { if ($this->getScheme()) { if ($this->uriParts['port'] == Constants::STANDARD_PORTS[$this->getScheme()]) { return NULL; } } return (int) $this->uriParts['port']; } }

    的要求 9. 路径是 DNS 地址后面的 URI 的部分。根据 PSR-7,必须对其进行编码。我们使用rawurlencode()PHP 函数,因为它符合 RFC3986。但是,我们不能只对整个路径进行编码,因为路径分隔符(即/也会进行编码!因此,我们需要首先使用explode()将其分解,对零件进行编码,然后重新组装:

    php public function getPath() { if (empty($this->urlParts['path'])) { return ''; } return implode('/', array_map("rawurlencode", explode('/', $this->urlParts['path']))); }

  9. 接下来,我们定义一个方法来检索query字符串(即从$_GET中)。这些也必须是 URL 编码的。首先,我们定义了getQueryParams(),它将查询字符串分解为一个关联数组。如果我们希望刷新查询参数,您将注意重置选项。然后我们定义getQuery(),它接受数组并生成正确的 URL 编码字符串:

    ```php public function getQueryParams($reset = FALSE) { if ($this->queryParams && !$reset) { return $this->queryParams; } $this->queryParams = []; if (!empty($this->uriParts['query'])) { foreach (explode('&', $this->uriParts['query']) as $keyPair) { list($param,$value) = explode('=',$keyPair); $this->queryParams[$param] = $value; } } return $this->queryParams; }

    public function getQuery() { if (!$this->getQueryParams()) { return ''; } $output = ''; foreach ($this->getQueryParams() as $key => $value) { $output .= rawurlencode($key) . '=' . rawurlencode($value) . '&'; } return substr($output, 0, -1); } ```

  10. 之后,我们提供了返回fragment(即 URI 中的#)的方法,以及它后面的任何部分:

    php public function getFragment() { if (empty($this->urlParts['fragment'])) { return ''; } return rawurlencode($this->urlParts['fragment']); }

  11. Next, we define a series of withXXX() methods, which match the getXXX() methods described above. These methods are designed to add, replace, or remove properties associated with the request class (scheme, authority, user info, and so on). In addition, these methods return the current instance that allows us to use these methods in a series of successive calls (often referred to as the fluent interface). We start with withScheme():

    您将注意到,根据 PSR-7,一个空参数表示删除该属性。您还将注意到,我们不允许与Constants::STANDARD_PORTS数组中定义的内容不匹配的方案。

    php public function withScheme($scheme) { if (empty($scheme) && $this->getScheme()) { unset($this->uriParts['scheme']); } else { if (isset(STANDARD_PORTS[strtolower($scheme)])) { $this->uriParts['scheme'] = $scheme; } else { throw new InvalidArgumentException(Constants::ERROR_BAD . __METHOD__); } } return $this; }

  12. 然后,我们将类似的逻辑应用于覆盖、添加或替换用户信息、主机、端口、路径、查询和片段的方法。注意,withQuery()方法重置查询参数数组。withHost()withPort()withPath()withFragment()使用相同的逻辑,但未显示以节省空间:

    ```php public function withUserInfo($user, $password = null) { if (empty($user) && $this->getUserInfo()) { unset($this->uriParts['user']); } else { $this->urlParts['user'] = $user; if ($password) { $this->urlParts['pass'] = $password; } } return $this; } // Not shown: withHost(),withPort(),withPath(),withFragment()

    public function withQuery($query) { if (empty($query) && $this->getQuery()) { unset($this->uriParts['query']); } else { $this->uriParts['query'] = $query; } // reset query params array $this->getQueryParams(TRUE); return $this; } ```

  13. 最后,我们用__toString()包装Application\MiddleWare\Uri类,当对象在字符串上下文中使用时,返回一个正确的 URI,由$uriParts组合而成。我们还定义了一个方便的方法getUriString(),它只调用__toString()

    php public function __toString() { $uri = ($this->getScheme()) ? $this->getScheme() . '://' : '';

  14. 如果authorityURI 部分存在,我们将添加它。authority包括用户信息、主机和端口。否则,我们只需追加hostport

    php if ($this->getAuthority()) { $uri .= $this->getAuthority(); } else { $uri .= ($this->getHost()) ? $this->getHost() : ''; $uri .= ($this->getPort()) ? ':' . $this->getPort() : ''; }

  15. Before adding path, we first check whether the first character is /. If not, we need to add this separator. We then add query and fragment, if present:

    ```php $path = $this->getPath(); if ($path) { if ($path[0] != '/') { $uri .= '/' . $path; } else { $uri .= $path; } } $uri .= ($this->getQuery()) ? '?' . $this->getQuery() : ''; $uri .= ($this->getFragment()) ? '#' . $this->getFragment() : ''; return $uri; }

    public function getUriString() { return $this->__toString(); }

    } ```

    请注意字符串解引用(即$path[0])的使用,它现在是 PHP7 的一部分。

  16. 接下来,我们将注意力转向一个表示消息主体的类。由于不知道尸体有多大,PSR-7 建议将尸体视为。流是一种允许以线性方式访问输入和输出源的资源。在 PHP 中,所有文件命令都在Streams子系统之上运行,因此这是一种自然的匹配。PSR-7 通过Psr\Http\Message\StreamInterface将其形式化,定义了read()write()seek()等方法。我们现在展示了可用于表示传入或传出请求和/或响应主体的Application\MiddleWare\Stream

    php namespace Application\MiddleWare; use SplFileInfo; use Throwable; use RuntimeException; use Psr\Http\Message\StreamInterface; class Stream implements StreamInterface { protected $stream; protected $metadata; protected $info;

  17. In the constructor, we open the stream using a simple fopen() command. We then use stream_get_meta_data() to get information on the stream. For other details, we create an SplFileInfo instance:

    php public function __construct($input, $mode = self::MODE_READ) { $this->stream = fopen($input, $mode); $this->metadata = stream_get_meta_data($this->stream); $this->info = new SplFileInfo($input); }

    我们选择fopen()而不是更现代的SplFileObject的原因是后者不允许直接访问内部文件资源对象,因此对该应用程序没有用处。

  18. 我们包括两种方便的方法,提供对资源的访问,以及对SplFileInfo实例

    ```php public function getStream() { return $this->stream; }

    public function getInfo() { return $this->info; } ```

    的访问 20. 接下来,我们定义了底层核心流方法:

    php public function read($length) { if (!fread($this->stream, $length)) { throw new RuntimeException(self::ERROR_BAD . __METHOD__); } } public function write($string) { if (!fwrite($this->stream, $string)) { throw new RuntimeException(self::ERROR_BAD . __METHOD__); } } public function rewind() { if (!rewind($this->stream)) { throw new RuntimeException(self::ERROR_BAD . __METHOD__); } } public function eof() { return eof($this->stream); } public function tell() { try { return ftell($this->stream); } catch (Throwable $e) { throw new RuntimeException(self::ERROR_BAD . __METHOD__); } } public function seek($offset, $whence = SEEK_SET) { try { fseek($this->stream, $offset, $whence); } catch (Throwable $e) { throw new RuntimeException(self::ERROR_BAD . __METHOD__); } } public function close() { if ($this->stream) { fclose($this->stream); } } public function detach() { return $this->close(); }

  19. 我们还需要定义告知流的信息方法:

    php public function getMetadata($key = null) { if ($key) { return $this->metadata[$key] ?? NULL; } else { return $this->metadata; } } public function getSize() { return $this->info->getSize(); } public function isSeekable() { return boolval($this->metadata['seekable']); } public function isWritable() { return $this->stream->isWritable(); } public function isReadable() { return $this->info->isReadable(); }

  20. 按照 PSR-7 指南,我们然后定义getContents()__toString()以转储流的内容:

    ```php public function __toString() { $this->rewind(); return $this->getContents(); }

    public function getContents() { ob_start(); if (!fpassthru($this->stream)) { throw new RuntimeException(self::ERROR_BAD . METHOD); } return ob_get_clean(); } } ```

  21. 前面显示的Stream类的一个重要变体是TextStream,它是为设计的,用于主体是字符串(即编码为 JSON 的数组)而不是文件的情况。由于我们需要绝对确保传入的$input值是字符串数据类型,因此我们在开始标记之后调用 PHP7 严格类型。我们还识别一个$pos属性(即位置),该属性将模拟文件指针,但会指向字符串中的一个位置:

    ```php <?php declare(strict_types=1); namespace Application\MiddleWare; use Throwable; use RuntimeException; use SplFileInfo; use Psr\Http\Message\StreamInterface;

    class TextStream implements StreamInterface { protected $stream; protected $pos = 0; ```

  22. 大多数方法都非常简单,并且不言自明。$stream属性是输入字符串:

    php public function __construct(string $input) { $this->stream = $input; } public function getStream() { return $this->stream; } public function getInfo() { return NULL; } public function getContents() { return $this->stream; } public function __toString() { return $this->getContents(); } public function getSize() { return strlen($this->stream); } public function close() { // do nothing: how can you "close" string??? } public function detach() { return $this->close(); // that is, do nothing! }

  23. 要模拟流行为、tell()eof()seek()等,请使用$pos

    php public function tell() { return $this->pos; } public function eof() { return ($this->pos == strlen($this->stream)); } public function isSeekable() { return TRUE; } public function seek($offset, $whence = NULL) { if ($offset < $this->getSize()) { $this->pos = $offset; } else { throw new RuntimeException( Constants::ERROR_BAD . __METHOD__); } } public function rewind() { $this->pos = 0; } public function isWritable() { return TRUE; }

  24. read()write()方法与$pos和子字符串

    ```php public function write($string) { $temp = substr($this->stream, 0, $this->pos); $this->stream = $temp . $string; $this->pos = strlen($this->stream); }

    public function isReadable() { return TRUE; } public function read($length) { return substr($this->stream, $this->pos, $length); } public function getMetadata($key = null) { return NULL; }

    } ```

    一起工作 27. 最后一个要显示的值对象是Application\MiddleWare\UploadedFile。与其他类一样,我们首先定义表示文件上载方面的属性:

    ```php namespace Application\MiddleWare; use RuntimeException; use InvalidArgumentException; use Psr\Http\Message\UploadedFileInterface; class UploadedFile implements UploadedFileInterface {

    protected $field; // original name of file upload field protected $info; // $_FILES[$field] protected $randomize; protected $movedName = ''; ```

  25. 在构造函数中,我们允许定义文件上传表单字段的 name 属性,以及$_FILES中对应的数组。我们添加最后一个参数,以表明我们是否希望该类在确认上传的文件后生成一个新的随机文件名:

    php public function __construct($field, array $info, $randomize = FALSE) { $this->field = $field; $this->info = $info; $this->randomize = $randomize; }

  26. 接下来,我们为临时或移动的文件创建一个Stream类实例:

    php public function getStream() { if (!$this->stream) { if ($this->movedName) { $this->stream = new Stream($this->movedName); } else { $this->stream = new Stream($info['tmp_name']); } } return $this->stream; }

  27. moveTo()方法执行实际的文件移动。注意大量的安全检查有助于防止注射攻击。如果未启用随机化,我们将使用原始用户提供的文件名:

    php public function moveTo($targetPath) { if ($this->moved) { throw new Exception(Constants::ERROR_MOVE_DONE); } if (!file_exists($targetPath)) { throw new InvalidArgumentException(Constants::ERROR_BAD_DIR); } $tempFile = $this->info['tmp_name'] ?? FALSE; if (!$tempFile || !file_exists($tempFile)) { throw new Exception(Constants::ERROR_BAD_FILE); } if (!is_uploaded_file($tempFile)) { throw new Exception(Constants::ERROR_FILE_NOT); } if ($this->randomize) { $final = bin2hex(random_bytes(8)) . '.txt'; } else { $final = $this->info['name']; } $final = $targetPath . '/' . $final; $final = str_replace('//', '/', $final); if (!move_uploaded_file($tempFile, $final)) { throw new RuntimeException(Constants::ERROR_MOVE_UNABLE); } $this->movedName = $final; return TRUE; }

  28. 然后我们提供对$_FILES中从$info属性返回的其他参数的访问。请注意,来自getClientFilename()getClientMediaType()的返回值应被视为不可信,因为它们来自外部。我们还添加了一个方法来返回移动的文件名:

    ```php public function getMovedName() { return $this->movedName ?? NULL; } public function getSize() { return $this->info['size'] ?? NULL; } public function getError() { if (!$this->moved) { return UPLOAD_ERR_OK; } return $this->info['error']; } public function getClientFilename() { return $this->info['name'] ?? NULL; } public function getClientMediaType() { return $this->info['type'] ?? NULL; }

    } ```

它是如何工作的。。。

首先,进入https://github.com/php-fig/http-message/tree/master/src ,用于 PSR-7 接口的 GitHub 存储库,并下载它们。在/path/to/source中创建一个名为Psr/Http/Message的目录,并将文件放在那里。或者,您可以访问https://packagist.org/packages/psr/http-message 并使用Composer安装源代码。(有关如何获取和使用Composer的说明,您可以访问https://getcomposer.org/

然后,继续定义前面讨论的类,总结在下表中:

|

|

中讨论的步骤

| | --- | --- | | Application\MiddleWare\Constants | 2. | | Application\MiddleWare\Uri | 3 至 16 | | Application\MiddleWare\Stream | 17 至 22 | | Application\MiddleWare\TextStream | 23 至 26 | | Application\MiddleWare\UploadedFile | 27 至 31 |

接下来,定义一个chap_09_middleware_value_objects_uri.php调用程序,该程序实现自动加载并使用适当的类。请注意,如果您使用Composer,除非另有说明,否则它将创建一个名为vendor的文件夹。Composer还添加了自己的自动加载器,您可以在此免费使用:

<?php
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\MiddleWare\Uri;

然后您可以创建一个Uri实例,并使用with方法添加参数。然后您可以直接回显Uri实例,因为__toString()已定义:

$uri = new Uri();
$uri->withScheme('https')
    ->withHost('localhost')
    ->withPort('8080')
    ->withPath('chap_09_middleware_value_objects_uri.php')
    ->withQuery('param=TEST');

echo $uri;

以下是预期结果:

How it works...

接下来,从/path/to/source/for/this/chapter创建一个名为uploads的目录。继续定义另一个调用程序chap_09_middleware_value_objects_file_upload.php,该程序设置自动加载并使用适当的类:

<?php
define('TARGET_DIR', __DIR__ . '/uploads');
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\MiddleWare\UploadedFile;

try...catch块内,检查是否上传了任何文件。如果是,则循环通过$_FILES并创建UploadedFile实例,其中设置了tmp_name。然后您可以使用moveTo()方法将文件移动到TARGET_DIR

try {
    $message = '';
    $uploadedFiles = array();
    if (isset($_FILES)) {
        foreach ($_FILES as $key => $info) {
          if ($info['tmp_name']) {
              $uploadedFiles[$key] = new UploadedFile($key, $info, TRUE);
              $uploadedFiles[$key]->moveTo(TARGET_DIR);
          }
        }
    }
} catch (Throwable $e) {
    $message =  $e->getMessage();
}
?>

在视图逻辑中,显示一个简单的文件上传表单。您还可以使用phpinfo(显示上传内容的相关信息:

<form name="search" method="post" enctype="<?= Constants::CONTENT_TYPE_MULTI_FORM ?>">
<table class="display" cellspacing="0" width="100%">
    <tr><th>Upload 1</th><td><input type="file" name="upload_1" /></td></tr>
    <tr><th>Upload 2</th><td><input type="file" name="upload_2" /></td></tr>
    <tr><th>Upload 3</th><td><input type="file" name="upload_3" /></td></tr>
    <tr><th>&nbsp;</th><td><input type="submit" /></td></tr>
</table>
</form>
<?= ($message) ? '<h1>' . $message . '</h1>' : ''; ?>

接下来,如果有上传的文件,可以显示每个文件的信息。您还可以使用getStream()后跟getContents()来显示每个文件(假设您使用的是短文本文件):

<?php if ($uploadedFiles) : ?>
<table class="display" cellspacing="0" width="100%">
    <tr>
        <th>Filename</th><th>Size</th>
      <th>Moved Filename</th><th>Text</th>
    </tr>
    <?php foreach ($uploadedFiles as $obj) : ?>
        <?php if ($obj->getMovedName()) : ?>
        <tr>
            <td><?= htmlspecialchars($obj->getClientFilename()) ?></td>
            <td><?= $obj->getSize() ?></td>
            <td><?= $obj->getMovedName() ?></td>
            <td><?= $obj->getStream()->getContents() ?></td>
        </tr>
        <?php endif; ?>
    <?php endforeach; ?>
</table>
<?php endif; ?>
<?php phpinfo(INFO_VARIABLES); ?>

以下是输出的显示方式:

How it works...

另见

开发 PSR-7 请求类

PSR-7 中间件的关键特性之一是使用请求响应类。当应用时,这使得软件的不同块可以一起执行,而不会在它们之间共享任何特定知识。在此上下文中,请求类应包含原始用户请求的所有方面,包括浏览器设置、请求的原始 URL、传递的参数等项。

怎么做。。。

  1. 首先,确保定义类来表示UriStreamUploadedFile值对象,如前一个配方中所述。
  2. 现在我们准备定义核心Application\MiddleWare\Message类。此类使用StreamUri并实现Psr\Http\Message\MessageInterface。我们首先定义键值对象的属性,包括表示消息体(即StreamInterface实例)、版本和 HTTP 头的属性:

    php namespace Application\MiddleWare; use Psr\Http\Message\ { MessageInterface, StreamInterface, UriInterface }; class Message implements MessageInterface { protected $body; protected $version; protected $httpHeaders = array();

  3. 接下来,我们有表示一个StreamInterface实例的getBody()方法。伴随方法withBody()返回当前Message实例,允许我们覆盖body

    php public function getBody() { if (!$this->body) { $this->body = new Stream(self::DEFAULT_BODY_STREAM); } return $this->body; } public function withBody(StreamInterface $body) { if (!$body->isReadable()) { throw new InvalidArgumentException(self::ERROR_BODY_UNREADABLE); } $this->body = $body; return $this; }

    的当前值 4. PSR-7 建议将标题视为不区分大小写。因此,我们定义了一种findHeader()方法(不是由MessageInterface直接定义的),该方法使用stripos()

    php protected function findHeader($name) { $found = FALSE; foreach (array_keys($this->getHeaders()) as $header) { if (stripos($header, $name) !== FALSE) { $found = $header; break; } } return $found; }

    定位报头 5. 下一种方法(不是 PSR-7 定义的方法)旨在填充$httpHeaders属性。假定此属性是一个关联数组,其中键是标头,值是表示标头值的字符串。如果有多个值,则会在字符串后面附加以逗号分隔的其他值。Apache 扩展中有一个很好的apache_request_headers()PHP 函数,如果$httpHeaders

    php protected function getHttpHeaders() { if (!$this->httpHeaders) { if (function_exists('apache_request_headers')) { $this->httpHeaders = apache_request_headers(); } else { $this->httpHeaders = $this->altApacheReqHeaders(); } } return $this->httpHeaders; }

    中还没有头文件,它会生成头文件 6. 如果apache_request_headers()不可用(即未启用 Apache 扩展),我们提供一个备选方案altApacheReqHeaders()

    php protected function altApacheReqHeaders() { $headers = array(); foreach ($_SERVER as $key => $value) { if (stripos($key, 'HTTP_') !== FALSE) { $headerKey = str_ireplace('HTTP_', '', $key); $headers[$this->explodeHeader($headerKey)] = $value; } elseif (stripos($key, 'CONTENT_') !== FALSE) { $headers[$this->explodeHeader($key)] = $value; } } return $headers; } protected function explodeHeader($header) { $headerParts = explode('_', $header); $headerKey = ucwords(implode(' ', strtolower($headerParts))); return str_replace(' ', '-', $headerKey); }

  4. 实现getHeaders()(PSR-7 中需要)现在是一个简单的循环,通过第 4 步

    php public function getHeaders() { foreach ($this->getHttpHeaders() as $key => $value) { header($key . ': ' . $value); } }

    中讨论的方法生成的$httpHeaders属性实现 8. 同样,我们提供了一系列with方法,用于覆盖或替换标题。因为可以有很多头,我们还有一个方法可以添加到现有的头集合中。withoutHeader()方法用于删除 header 实例。注意前面步骤中提到的一致使用findHeader(),以允许对头进行不区分大小写的处理:

    ```php public function withHeader($name, $value) { $found = $this->findHeader($name); if ($found) { $this->httpHeaders[$found] = $value; } else { $this->httpHeaders[$name] = $value; } return $this; }

    public function withAddedHeader($name, $value) { $found = $this->findHeader($name); if ($found) { $this->httpHeaders[$found] .= $value; } else { $this->httpHeaders[$name] = $value; } return $this; }

    public function withoutHeader($name) { $found = $this->findHeader($name); if ($found) { unset($this->httpHeaders[$found]); } return $this; } ```

  5. 然后,根据 PSR-7:

    ```php public function hasHeader($name) { return boolval($this->findHeader($name)); }

    public function getHeaderLine($name) { $found = $this->findHeader($name); if ($found) { return $this->httpHeaders[$found]; } else { return ''; } }

    public function getHeader($name) { $line = $this->getHeaderLine($name); if ($line) { return explode(',', $line); } else { return array(); } } ```

    ,我们提供了一系列有用的与报头相关的方法来确认报头存在,检索单个报头行,并以数组形式检索报头 10. 最后,为了对进行四舍五入的头处理,我们提出了getHeadersAsString,它生成了一个头字符串,头由\r\n分隔,以便直接用于 PHP 流上下文:

    php public function getHeadersAsString() { $output = ''; $headers = $this->getHeaders(); if ($headers && is_array($headers)) { foreach ($headers as $key => $value) { if ($output) { $output .= "\r\n" . $key . ': ' . $value; } else { $output .= $key . ': ' . $value; } } } return $output; }

  6. Message类中,我们现在将注意力转向版本处理。根据 PSR-7,协议版本(即 HTTP/1.1)的返回值应仅为数字部分。因此,我们还提供了去除任何非数字字符的onlyVersion(),允许句点:

    ```php public function getProtocolVersion() { if (!$this->version) { $this->version = $this->onlyVersion($_SERVER['SERVER_PROTOCOL']); } return $this->version; }

    public function withProtocolVersion($version) { $this->version = $this->onlyVersion($version); return $this; }

    protected function onlyVersion($version) { if (!empty($version)) { return preg_replace('/[^0-9.]/', '', $version); } else { return NULL; } }

    } ```

  7. 最后,我们准备定义我们的Request类,这几乎是一个扫兴。但是,这里必须注意,我们需要考虑绑定和绑定请求。也就是说,我们需要一个类来表示客户机将向服务器发出的传出请求,以及服务器从客户机收到的请求*。因此,我们提供Application\MiddleWare\Request(客户端将向服务器发出的请求)和Application\MiddleWare\ServerRequest(服务器从客户端接收的请求)。好消息是我们的大部分工作已经完成:注意我们的Request类扩展了Message。我们还提供了表示 URI 和 HTTP 方法的属性:

    ```php namespace Application\MiddleWare;

    use InvalidArgumentException; use Psr\Http\Message\ { RequestInterface, StreamInterface, UriInterface };

    class Request extends Message implements RequestInterface { protected $uri; protected $method; // HTTP method protected $uriObj; // Psr\Http\Message\UriInterface instance ``* 13. 构造函数中的所有属性都默认为NULL,但我们保留立即定义适当参数的可能性。我们使用继承的onlyVersion()方法来清理版本。我们还定义了checkMethod()以确保提供的任何方法都在我们支持的 HTTP 方法列表中,在Constants`:

    php public function __construct($uri = NULL, $method = NULL, StreamInterface $body = NULL, $headers = NULL, $version = NULL) { $this->uri = $uri; $this->body = $body; $this->method = $this->checkMethod($method); $this->httpHeaders = $headers; $this->version = $this->onlyVersion($version); } protected function checkMethod($method) { if (!$method === NULL) { if (!in_array(strtolower($method), Constants::HTTP_METHODS)) { throw new InvalidArgumentException(Constants::ERROR_HTTP_METHOD); } } return $method; }

    中定义为常量数组 14. 我们将以字符串的形式将请求目标解释为最初请求的 URI。请记住,我们的Uri类具有将其解析为其组成部分的方法,因此我们提供了$uriObj属性。在withRequestTarget()的情况下,请注意,我们运行了执行上述解析过程的getUri()

    ```php public function getRequestTarget() { return $this->uri ?? Constants::DEFAULT_REQUEST_TARGET; }

    public function withRequestTarget($requestTarget) { $this->uri = $requestTarget; $this->getUri(); return $this; } ```

  8. 我们的getwith方法(代表 HTTP 方法)没有令人惊讶的地方。我们使用checkMethod(),也在构造函数中使用,以确保方法与我们计划支持的方法相匹配:

    ```php public function getMethod() { return $this->method; }

    public function withMethod($method) { $this->method = $this->checkMethod($method); return $this; } ```

  9. 最后,我们有一个用于 URI 的方法getwith方法。如步骤 14 所述,我们在$uri属性中保留原始请求字符串,在$uriObj中保留新解析的Uri实例。注意保留任何现有Host标题的额外标志:

    ```php public function getUri() { if (!$this->uriObj) { $this->uriObj = new Uri($this->uri); } return $this->uriObj; }

    public function withUri(UriInterface $uri, $preserveHost = false) { if ($preserveHost) { $found = $this->findHeader(Constants::HEADER_HOST); if (!$found && $uri->getHost()) { $this->httpHeaders[Constants::HEADER_HOST] = $uri->getHost(); } } elseif ($uri->getHost()) { $this->httpHeaders[Constants::HEADER_HOST] = $uri->getHost(); } $this->uri = $uri->__toString(); return $this; } } ```

  10. ServerRequest类扩展了Request并提供了额外的功能来检索处理传入请求的服务器感兴趣的信息。我们首先定义表示从各种 PHP$_ super-globals(即$_SERVER$_POST等)读取的传入数据的属性:

    ```php namespace Application\MiddleWare; use Psr\Http\Message\ { ServerRequestInterface, UploadedFileInterface } ;

    class ServerRequest extends Request implements ServerRequestInterface {

    protected $serverParams; protected $cookies; protected $queryParams; protected $contentType; protected $parsedBody; protected $attributes; protected $method; protected $uploadedFileInfo; protected $uploadedFileObjs; ```

  11. 然后,我们定义一系列 getter 来提取超级全局信息。我们不展示一切,以节省空间:

    ```php public function getServerParams() { if (!$this->serverParams) { $this->serverParams = $_SERVER; } return $this->serverParams; } // getCookieParams() reads $_COOKIE // getQueryParams() reads $_GET // getUploadedFileInfo() reads $_FILES

    public function getRequestMethod() { $method = $this->getServerParams()['REQUEST_METHOD'] ?? ''; $this->method = strtolower($method); return $this->method; }

    public function getContentType() { if (!$this->contentType) { $this->contentType = $this->getServerParams()['CONTENT_TYPE'] ?? ''; $this->contentType = strtolower($this->contentType); } return $this->contentType; } ```

  12. 由于上传的文件应该表示为独立UploadedFile对象(在前面的配方中介绍),我们还定义了一个方法,该方法接受$uploadedFileInfo并创建UploadedFile对象:

    php public function getUploadedFiles() { if (!$this->uploadedFileObjs) { foreach ($this->getUploadedFileInfo() as $field => $value) { $this->uploadedFileObjs[$field] = new UploadedFile($field, $value); } } return $this->uploadedFileObjs; }

  13. 与前面定义的其他类一样,我们提供了添加或覆盖属性并返回新实例的with方法:

    php public function withCookieParams(array $cookies) { array_merge($this->getCookieParams(), $cookies); return $this; } public function withQueryParams(array $query) { array_merge($this->getQueryParams(), $query); return $this; } public function withUploadedFiles(array $uploadedFiles) { if (!count($uploadedFiles)) { throw new InvalidArgumentException(Constant::ERROR_NO_UPLOADED_FILES); } foreach ($uploadedFiles as $fileObj) { if (!$fileObj instanceof UploadedFileInterface) { throw new InvalidArgumentException(Constant::ERROR_INVALID_UPLOADED); } } $this->uploadedFileObjs = $uploadedFiles; }

  14. PSR-7 消息的一个重要方面是,正文也应该以解析的方式提供,也就是说,一种结构化表示,而不仅仅是原始流。因此,我们定义了getParsedBody()及其伴随的with方法。PSR-7 建议在表单发布方面非常具体。注意检查Content-Type标题的if语句系列以及方法:

    ```php public function getParsedBody() { if (!$this->parsedBody) { if (($this->getContentType() == Constants::CONTENT_TYPE_FORM_ENCODED || $this->getContentType() == Constants::CONTENT_TYPE_MULTI_FORM) && $this->getRequestMethod() == Constants::METHOD_POST) { $this->parsedBody = $_POST; } elseif ($this->getContentType() == Constants::CONTENT_TYPE_JSON || $this->getContentType() == Constants::CONTENT_TYPE_HAL_JSON) { ini_set("allow_url_fopen", true); $this->parsedBody = json_decode(file_get_contents('php://input')); } elseif (!empty($_REQUEST)) { $this->parsedBody = $_REQUEST; } else { ini_set("allow_url_fopen", true); $this->parsedBody = file_get_contents('php://input'); } } return $this->parsedBody; }

    public function withParsedBody($data) { $this->parsedBody = $data; return $this; } ```

  15. 我们还允许 PSR-7 中未精确定义的属性使用。相反,我们将此保持打开状态,以便开发人员可以提供适合应用程序的任何内容。请注意,withoutAttributes()允许您随意删除属性:

    ```php public function getAttributes() { return $this->attributes; } public function getAttribute($name, $default = NULL) { return $this->attributes[$name] ?? $default; } public function withAttribute($name, $value) { $this->attributes[$name] = $value; return $this; } public function withoutAttribute($name) { if (isset($this->attributes[$name])) { unset($this->attributes[$name]); } return $this; }

    } ```

  16. 最后,为了从绑定请求中加载不同的属性,我们定义了initialize(),它不在 PSR-7 中,但非常方便:

    php public function initialize() { $this->getServerParams(); $this->getCookieParams(); $this->getQueryParams(); $this->getUploadedFiles; $this->getRequestMethod(); $this->getContentType(); $this->getParsedBody(); return $this; }

它是如何工作的。。。

首先,确保完成前面的配方,因为MessageRequest类消耗UriStreamUploadedFile值对象。之后,继续定义下表中总结的类:

|

|

这些步骤将在中讨论

| | --- | --- | | Application\MiddleWare\Message | 2 至 9 | | Application\MiddleWare\Request | 10 至 14 | | Application\MiddleWare\ServerRequest | 15 至 20 |

之后,您可以定义一个服务器程序chap_09_middleware_server.php,它设置自动加载并使用适当的类。此脚本将传入请求拉入ServerRequest实例,初始化它,然后使用var_dump()显示接收到的信息:

<?php
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\MiddleWare\ServerRequest;

$request = new ServerRequest();
$request->initialize();
echo '<pre>', var_dump($request), '</pre>';

要运行服务器程序,首先更改为/path/to/source/for/this/chapter folder。然后可以运行以下命令:

php -S localhost:8080 chap_09_middleware_server.php'

对于客户端,首先创建一个调用程序chap_09_middleware_request.php,该程序设置自动加载,使用适当的类,并定义目标服务器和本地文本文件:

<?php
define('READ_FILE', __DIR__ . '/gettysburg.txt');
define('TEST_SERVER', 'http://localhost:8080');
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\MiddleWare\ { Request, Stream, Constants };

接下来,您可以使用文本作为源创建一个Stream实例。这将成为新请求的主体,在本例中,它反映了表单发布的预期内容:

$body = new Stream(READ_FILE);

然后可以直接构建一个Request实例,根据需要提供参数:

$request = new Request(
    TEST_SERVER,
    Constants::METHOD_POST,
    $body,
    [Constants::HEADER_CONTENT_TYPE => Constants::CONTENT_TYPE_FORM_ENCODED,Constants::HEADER_CONTENT_LENGTH => $body->getSize()]
);

或者,您可以使用 fluent 接口语法生成完全相同的结果:

$uriObj = new Uri(TEST_SERVER);
$request = new Request();
$request->withRequestTarget(TEST_SERVER)
        ->withMethod(Constants::METHOD_POST)
        ->withBody($body)
        ->withHeader(Constants::HEADER_CONTENT_TYPE, Constants::CONTENT_TYPE_FORM_ENCODED)
        ->withAddedHeader(Constants::HEADER_CONTENT_LENGTH, $body->getSize());

然后可以设置一个 cURL 资源来模拟表单发布,其中数据参数是文本文件的内容。您可以通过curl_init()curl_exec()等方式进行跟踪,以响应结果:

$data = http_build_query(['data' => $request->getBody()->getContents()]);
$defaults = array(
    CURLOPT_URL => $request->getUri()->getUriString(),
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $data,
);
$ch = curl_init();
curl_setopt_array($ch, $defaults);
$response = curl_exec($ch);
curl_close($ch);

以下是直接输出的显示方式:

How it works...

另见

定义 PSR-7 响应等级

响应类表示返回给发出原始请求的任何实体的出站信息。HTTP 头在上下文中起着重要作用,因为我们需要知道客户端请求的格式,通常在传入的Accept头中。然后,我们需要在响应类中设置适当的Content-Type头以匹配该格式。否则,响应的实际主体将是 HTML、JSON 或任何其他已请求(并已交付)的内容。

怎么做。。。

  1. Response类实际上比Request类更容易实现,因为我们只关心从服务器返回响应到客户端。此外,它扩展了我们的Application\MiddleWare\Message类,其中大部分工作已经完成。所以,剩下要做的就是定义一个Application\MiddleWare\Response类。正如您将注意到的,唯一的唯一属性是$statusCode

    php namespace Application\MiddleWare; use Psr\Http\Message\ { Constants, ResponseInterface, StreamInterface }; class Response extends Message implements ResponseInterface { protected $statusCode;

  2. PSR-7 没有定义构造函数,但我们提供它是为了方便,允许开发人员创建一个所有部分都完好无损的Response实例。我们使用Message中的方法和Constants类中的常量来验证参数:

    php public function __construct($statusCode = NULL, StreamInterface $body = NULL, $headers = NULL, $version = NULL) { $this->body = $body; $this->status['code'] = $statusCode ?? Constants::DEFAULT_STATUS_CODE; $this->status['reason'] = Constants::STATUS_CODES[$statusCode] ?? ''; $this->httpHeaders = $headers; $this->version = $this->onlyVersion($version); if ($statusCode) $this->setStatusCode(); }

  3. 我们提供了一种很好的方法来设置 HTTP 状态代码,而不考虑任何头,使用 PHP5.4 以后提供的http_response_code()。因为这项工作是在 PHP7 上进行的,所以我们知道这种方法是安全的:

    php public function setStatusCode() { http_response_code($this->getStatusCode()); }

  4. 否则,可以使用以下方法获取状态代码:

    php public function getStatusCode() { return $this->status['code']; }

  5. 与前面配方中讨论的其他基于 PSR-7 的类一样,我们还定义了一个with方法,用于设置状态代码并返回当前实例。注意使用STATUS_CODES的来确认其存在:

    php public function withStatus($statusCode, $reasonPhrase = '') { if (!isset(Constants::STATUS_CODES[$statusCode])) { throw new InvalidArgumentException(Constants::ERROR_INVALID_STATUS); } $this->status['code'] = $statusCode; $this->status['reason'] = ($reasonPhrase) ? Constants::STATUS_CODES[$statusCode] : NULL; $this->setStatusCode(); return $this; }

  6. 最后,我们定义了一个方法,该方法返回 HTTP 状态的原因,在本例中是一个简短的文本短语,基于 RFC 7231。注意使用了 PHP7 null 合并运算符??,它返回三个可能选项中的第一个非空项:

    php public function getReasonPhrase() { return $this->status['reason'] ?? Constants::STATUS_CODES[$this->status['code']] ?? ''; } }

它是如何工作的。。。

首先,确保定义前两个菜谱中讨论的类。之后,您可以创建另一个简单的服务器程序chap_09_middleware_server_with_response.php,该程序设置自动加载并使用适当的类:

<?php
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
use Application\MiddleWare\ { Constants, ServerRequest, Response, Stream };

然后,您可以使用键/值对定义一个数组,其中值指向当前目录中要用作内容的文本文件:

$data = [
  1 => 'churchill.txt',
  2 => 'gettysburg.txt',
  3 => 'star_trek.txt'
];

接下来,在try...catch块中,您可以初始化一些变量,初始化服务器请求,并设置临时文件名:

try {

    $body['text'] = 'Initial State';
    $request = new ServerRequest();
    $request->initialize();
    $tempFile = bin2hex(random_bytes(8)) . '.txt';
    $code = 200;

之后,检查方法是 GET 还是 POST。如果是 GET,检查是否传递了id参数。如果是,则返回匹配文本文件的正文。否则,返回文本文件列表:

if ($request->getMethod() == Constants::METHOD_GET) {
    $id = $request->getQueryParams()['id'] ?? NULL;
    $id = (int) $id;
    if ($id && $id <= count($data)) {
        $body['text'] = file_get_contents(
        __DIR__ . '/' . $data[$id]);
    } else {
        $body['text'] = $data;
    }

否则,返回一个响应,指示成功代码 204 和接收到的请求正文的大小:

} elseif ($request->getMethod() == Constants::METHOD_POST) {
    $size = $request->getBody()->getSize();
    $body['text'] = $size . ' bytes of data received';
    if ($size) {
        $code = 201;
    } else {
        $code = 204;
    }
}

然后,您可以捕获任何异常并报告它们,状态代码为 500:

} catch (Exception $e) {
    $code = 500;
    $body['text'] = 'ERROR: ' . $e->getMessage();
}

响应需要包装在一个流中,因此您可以将主体写入临时文件,并将其创建为Stream。您还可以将Content-Type头设置为application/json并运行getHeaders(),输出当前的头集。然后,回显响应的主体。对于本例,您还可以转储Response实例以确认其构造正确:

try {
    file_put_contents($tempFile, json_encode($body));
    $body = new Stream($tempFile);
    $header[Constants::HEADER_CONTENT_TYPE] = 'application/json';
    $response = new Response($code, $body, $header);
    $response->getHeaders();
    echo $response->getBody()->getContents() . PHP_EOL;
    var_dump($response);

总结一下,使用Throwable捕捉任何错误或异常,不要忘记删除临时文件:

} catch (Throwable $e) {
    echo $e->getMessage();
} finally {
   unlink($tempFile);
}

要进行测试,只需打开一个终端窗口,切换到/path/to/source/for/this/chapter目录,然后运行以下命令:

php -S localhost:8080

然后,您可以通过浏览器调用此程序,添加id参数。您可以考虑打开开发工具来监视响应头。下面是一个预期输出的示例。注意application/json的内容类型:

How it works...

另见