PHP中PSR规范


PHP中PSR规范


正文

PSR 是由 PHP FIG 组织制定的 PHP 规范,是 PHP 开发的实践标准。

PHP FIG

PSR 是 PHP Standard Recommendations (PHP 推荐标准)的简写,由 PHP FIG 组织制定的 PHP 规范,是 PHP 开发的实践标准。

PHP FIG,FIG 是 Framework Interoperability Group(框架可互用性小组)的缩写,由几位开源框架的开发者成立于 2009 年, 从那开始也选取了很多其他成员进来(包括但不限于 Laravel, Joomla, Drupal, Composer, Phalcon, Slim, Symfony, Zend Framework 等), 虽然不是「官方」组织,但也代表了大部分的 PHP 社区。

项目的目的在于:通过框架作者或者框架的代表之间讨论,以最低程度的限制,制定一个协作标准,各个框架遵循统一的编码规范, 避免各家自行发展的风格阻碍了 PHP 的发展,解决这个程序设计师由来已久的困扰。

官方口号:通过协作和标准推动 PHP 向前发展。

目前已表决通过了不少套标准,已经得到大部分 PHP 框架的支持和认可。

本项目的主要面向对象是所有参与的各个成员(也就是各自框架的社区),这里是完整的 成员列表,当然,同时也欢迎其它 PHP 社区采用本规范。

状态说明

在本文档的文章列表中,文章标题会有规范状态的标示。所有的规范分为以下几个状态:

已通过(ACCEPTED)—— 文章标题将不会有所显示
起草中(DRAFT)—— 暂不翻译
未通过(ABANDONED)—— 简单说明,暂不翻译
已弃用(DEPRECATED)—— 会有说明,暂不翻译

规范目录

PSR-0 自动加载规范(已弃用)
PSR-1 基础编码规范(已通过)
PSR-2 编码风格规范(已弃用)
PSR-3 日志接口规范(已通过)
PSR-4 自动加载规范(已通过)
PSR-4 自动加载规范 - 示例文档 
PSR-4 自动加载规范 - 说明文档
PSR-5 PHPDoc 标准(起草中)
PSR-6 缓存接口规范(已通过)
PSR-6 缓存接口规范 - 说明文档 
PSR-7 HTTP 消息接口规范(已通过)
PSR-8 Huggable 接口(未通过)
PSR-9 项目安全问题公示(未通过)
PSR-10 项目安全上报方法(未通过)
PSR-11 容器接口(已通过)
PSR-11 容器接口 - 说明文档 
PSR-12 编码规范扩充(已通过)
PSR-13 超媒体链接(已通过)
PSR-13 超媒体链接 - 说明文档
PSR-14 事件分发器(已通过)
PSR-15 HTTP 请求处理器(已通过)
PSR-16 缓存接口(已通过)
PSR-16 缓存接口 - 说明文档
PSR-17 HTTP 工厂(已通过)
PSR-18 HTTP 客户端(已通过) 
PSR-19 PHPDoc tags(起草中)
PSR-20 Clock(起草中)
PSR-21 Internationalization(起草中)
PSR-22 Application Tracing(起草中)

大致分类

自动加载:自动加载器通过将命名空间映射到文件系统路径来消除包含文件的复杂性。

PSR-4 自动加载规范

接口:接口通过遵循预期的方案简化了项目之间的代码共享。

PSR-3 日志接口规范 
PSR-6 缓存接口规范 
PSR-11 容器接口
PSR-13 超媒体链接
PSR-14 事件分发器
PSR-16 缓存接口

HTTP:可互操作的标准和接口,以不可知的方式处理客户端和服务器端的HTTP请求和响应。

PSR-7 HTTP 消息接口规范
PSR-15 HTTP 请求处理器
PSR-17 HTTP 工厂 
PSR-18 HTTP 客户端  

编码风格:标准化格式减少了阅读其他作者的代码时的认知摩擦。

PSR-1 基础编码规范
PSR-12 编码规范扩充

PSR-1 基础编码规范

基本代码规范

本篇规范制定了代码基本元素的相关标准,以确保共享的 PHP 代码间具有较高程度的技术互通性。

本文件中的 必须,不得,需要,应,不应,应该,不应该,推荐,可能 和 可选 等能愿动词按照 RFC 2119 中的描述进行解释。

1、 概览

  • PHP 代码文件 必须 以 <?php<?= 标签开始;

  • PHP 代码文件 必须 以 不带 BOM 的 UTF-8 编码;

  • PHP 代码中 应该 声明任一标志(类、函数、常量等),或引起副作用(如果一个函数修改了自己范围之外的资源, 那就叫做有副作用,如:生成输出以及修改 .ini 配置文件等),但是不应该二者都有;

  • 命名空间以及类 必须 符合 PSR 的自动加载规范: [PSR-0(已废弃)或 PSR-4] 中的一个。

  • 类的命名 必须 遵循 StudlyCaps 大写开头的驼峰命名规范;

  • 类中的常量所有字母都 必须 大写,单词间用下划线分隔;

  • 方法名称 必须 符合 camelCase 式的小写开头驼峰命名规范。

2、文件

2.1. PHP 标签

PHP 代码 必须 使用 <?php ?> 长标签 或 <?= ?> 短输出标签;

一定不可 使用其它自定义标签。

2.2. 字符集编码

PHP 代码 必须 且只可使用 不带 BOM 的 UTF-8 编码。

2.3. 副作用

一份 PHP 文件中 应该 要不就只定义新的声明,如类、函数或常量等不产生 副作用 的操作, 要不就只执行会产生 副作用 的逻辑操作,但 不该 同时具有两者。

「副作用」(side effects) 一词的意思是,仅仅通过包含文件,不直接声明类、函数和常量等,而执行的逻辑操作。

「副作用」包含却不仅限于:生成输出,明确使用 require 或 include,连接到外部服务, 修改 ini 设置,发出错误或异常,修改全局或静态变量,读取或写入一个文件,等等。

以下是一个 反例,一份包含「函数声明」以及产生「副作用」的代码:

<?php
// 「副作用」:修改 ini 配置
ini_set('error_reporting', E_ALL);

// 「副作用」:引入文件
include "file.php";

// 「副作用」:生成输出
echo "<html>\n";

// 声明函数
function foo()
{
    // function body
}

下面是一个范例,一份只包含声明不产生「副作用」的代码:

<?php
// 声明函数
function foo()
{
    // 函数主体部分
}

// 条件声明 **不** 属于「副作用」
if (! function_exists('bar')) {
    function bar()
    {
        // 函数主体部分
    }
}

3、 命名空间和类名

命名空间和类名 必须 遵循『自动加载』规范: [PSR-0, PSR-4]。

这意味着每个类都独立为一个文件,并且至少在一个层次的命名空间内,那就是:顶级组织名(vendor name)。

类名 必须 以类似 StudlyCaps 形式的大写开头的驼峰命名方式声明。

PHP 5.3 及更高版本的代码 必须 使用正式的命名空间。

举个例子:

<?php
// PHP 5.3 及更高版本:
namespace Vendor\Model;

class Foo
{
}

PHP 5.2 及更低版本 应该 使用伪命名空间,约定俗成,以顶级组织名称 Vendor_ 为类名前缀:

<?php
// PHP 5.2.x 及更低版本:
class Vendor_Model_Foo
{
}

4、类的常量、属性和方法

此处的「类」指代所有的类、接口以及可复用代码块(traits)。

4.1. 常量

类的常量中所有字母都 必须 大写,词间以下划线分隔。例如:

<?php
namespace Vendor\Model;

class Foo
{
    const VERSION = '1.0';
    const DATE_APPROVED = '2012-06-01';
}

4.2. 属性

类的属性命名 可以 遵循:

大写开头的驼峰式 ($StudlyCaps)
小写开头的驼峰式 ($camelCase)
下划线分隔式 ($under_score)

本规范不做强制要求,但无论遵循哪种命名方式,都 应该 在一定的范围内保持一致。这个范围可以是整个团队、整个包、整个类或整个方法。

4.3. 方法

方法名称 必须 符合 camelCase() 式的小写开头驼峰命名规范。

PSR-3 日志接口规范

日志接口

本文制定了日志类库的通用接口规范。

本规范的主要目的,是为了让日志类库以简单通用的方式,通过接收一个 Psr\Log\LoggerInterface 对象,来记录日志信息。 框架以及 CMS 内容管理系统如有需要,可以 对此接口进行扩展,但需遵循本规范, 这才能保证在使用第三方的类库文件时,日志接口仍能正常对接。

为了避免歧义,文档大量使用了「能愿动词」,对应的解释如下:

必须 (MUST):绝对,严格遵循,请照做,无条件遵守;
一定不可 (MUST NOT):禁令,严令禁止;
应该 (SHOULD) :强烈建议这样做,但是不强求;
不该 (SHOULD NOT):强烈不建议这样做,但是不强求;
可以 (MAY) 和 可选 (OPTIONAL) :选择性高一点,在这个文档内,此词语使用较少;
参见  RFC 2119 .

本文档中的 implementor 一词应理解为在日志的库中实现 LoggerInterface 的人。记录器的调用者称为 user

1、规范

1.1 基本规范

  • LoggerInterface 接口对外定义了八个方法,分别用来记录 RFC 5424 中定义的八个等级的日志: debug、 info、 notice、 warning、 error、 critical、 alert 以及 emergency 。

  • 第九个方法 —— log,其第一个参数为记录的等级。可使用一个预先定义的等级常量作为参数来调用此方法, 必须 与直接调用以上八个方法具有相同的效果。如果传入的等级常量参数没有预先定义, 则 必须 抛出 Psr\Log\InvalidArgumentException 类型的异常。 在不确定的情况下,使用者 不该 使用未支持的等级常量来调用此方法。

1.2 消息

  • 以上每个方法都接受一个字符串类型或者是有 __toString() 方法的对象作为记录信息参数, 这样,实现者就能把它当成字符串来处理,否则实现者 必须 自己把它转换成字符串。

  • 记录信息参数 可以 携带占位符,实现者 可以 根据上下文将其它替换成相应的值。

其中占位符 必须 与上下文数组中的键名保持一致。

占位符的名称 必须 由一个左花括号 { 以及一个右括号 } 包含。但花括号与名称之间 一定不可有空格符。

占位符的名称 应该 只由 A-Z、a-z、0-9、下划线 _、以及英文的句号 . 组成,其它字符作为将来占位符规范的保留。

实现者 可以 通过对占位符采用不同的转义和转换策略,来生成最终的日志。 而使用者在不知道上下文的前提下,不该 提前转义占位符。

以下是一个占位符使用的例子:

<?php

/**
 * 用上下文信息替换记录信息中的占位符
 */
function interpolate($message, array $context = array())
{
    // 构建一个花括号包含的键名的替换数组
    $replace = array();
    foreach ($context as $key => $val) {
        // 检查该值是否可以转换为字符串
        if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) {
            $replace['{' . $key . '}'] = $val;
        }
    }

    // 替换记录信息中的占位符,最后返回修改后的记录信息。
    return strtr($message, $replace);
}

// 含有带花括号占位符的记录信息。
$message = "User {username} created";

// 带有替换信息的上下文数组,键名为占位符名称,键值为替换值。
$context = array('username' => 'bolivar');

// 输出 "User bolivar created"
echo interpolate($message, $context);

1.3 上下文

  • 每个记录函数都接受一个上下文数组参数,用来装载字符串类型无法表示的信息。 它 可以 装载任何信息,所以实现者 必须 确保能正确处理其装载的信息,对于其装载的数据, 一定不可 抛出异常,或产生 PHP 出错、警告或提醒信息(error、warning、notice)。

  • 如需通过上下文参数传入了一个 Exception 对象,必须 以 exception 作为键名。 记录异常信息是很普遍的,所以如果它能够在记录类库的底层实现,就能够让实现者从异常信息中抽丝剥茧。 当然,实现者在使用它时,必须 确保键名为 exception 的键值是否真的是一个 Exception,毕竟它 可以 装载任何信息。

1.4 助手类和接口

  • Psr\Log\AbstractLogger 类使得只需继承它和实现其中的 log 方法,就能够很轻易地实现 LoggerInterface 接口, 而另外八个方法就能够把记录信息和上下文信息传给它。

  • 同样地,使用 Psr\Log\LoggerTrait 也只需实现其中的 log 方法。不过,需要特别注意的是, 在 traits 可复用代码块还不能实现接口前,还需要 implement LoggerInterface

  • 在没有可用的日志记录器时,Psr\Log\NullLogger 接口 可以 为使用者提供一个备用的日志「黑洞」。 不过,当上下文的构建非常消耗资源时,带条件检查的日志记录或许是更好的办法。

  • Psr\Log\LoggerAwareInterface 接口仅包括一个 setLogger(LoggerInterface $logger) 方法,框架可以使用它实现自动连接任意的日志记录实例。

  • Psr\Log\LoggerAwareTrait trait 可复用代码块可以在任何的类里面使用,只需通过它提供的 $this->logger,就可以轻松地实现等同的接口。

  • Psr\Log\LogLevel 类装载了八个记录等级常量。

2、包

接口和类的描述、相关的异常类以及用于验证你所写代码的测试套件都将作为 psr/log 包的一部分提供。

3、Psr\Log\LoggerInterface

<?php

namespace Psr\Log;

/**
 * 描述一个日志记录器实例
 *
 * 该消息必须实现一个__toString()的字符串或者对象.
 *
 * 该消息可能包含以下形式的占位符: {foo}  
 * foo 将会被关键词 "foo"中的上下文数据替换.
 *
 * 上下文数组可以包含任意数据, 我们只能假设代码实现者如果给出一个生成堆栈跟踪的异常实例, 
 * 那么它的键名必须为 "exception"。
 *
 * 请前往 https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
 * 查看完整的接口规范.
 */
interface LoggerInterface
{
    /**
     * 系统无法使用。
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function emergency($message, array $context = array());

    /**
     * 必须立即采取行动。
     *
     * 例如: 整个网站宕机了,数据库挂了,等等。 这应该发送短信通知警告你.
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function alert($message, array $context = array());

    /**
     * 临界条件。
     *
     * 例如: 应用组件不可用,意外的异常。
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function critical($message, array $context = array());

    /**
     * 运行时错误不需要马上处理,但通常应该被记录和监控。
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function error($message, array $context = array());

    /**
     * 例外事件不是错误。
     *
     * 例如: 使用过时的API,API使用不当,不合理的东西不一定是错误。
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function warning($message, array $context = array());

    /**
     * 正常但重要的事件.
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function notice($message, array $context = array());

    /**
     * 有趣的事件.
     *
     * 例如: 用户登录,SQL日志。
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function info($message, array $context = array());

    /**
     * 详细的调试信息。
     *
     * @param string $message
     * @param array $context
     * @return void
     */
    public function debug($message, array $context = array());

    /**
     * 可任意级别记录日志。
     *
     * @param mixed $level
     * @param string $message
     * @param array $context
     * @return void
     */
    public function log($level, $message, array $context = array());
}

4、Psr\Log\LoggerAwareInterface

<?php

namespace Psr\Log;

/**
 * logger-aware 定义实例
 */
interface LoggerAwareInterface
{
    /**
     * 设置一个日志记录实例
     *
     * @param LoggerInterface $logger
     * @return void
     */
    public function setLogger(LoggerInterface $logger);
}

5、Psr\Log\LogLevel

<?php

namespace Psr\Log;

/**
 * 日志等级常量定义
 */
class LogLevel
{
    const EMERGENCY = 'emergency';
    const ALERT     = 'alert';
    const CRITICAL  = 'critical';
    const ERROR     = 'error';
    const WARNING   = 'warning';
    const NOTICE    = 'notice';
    const INFO      = 'info';
    const DEBUG     = 'debug';
}

PSR-4 自动加载规范

自动加载

为了避免歧义,文档大量使用了「能愿动词」,对应的解释如下:

必须 (MUST):绝对,严格遵循,请照做,无条件遵守;
一定不可 (MUST NOT):禁令,严令禁止;
应该 (SHOULD) :强烈建议这样做,但是不强求;
不该 (SHOULD NOT):强烈不建议这样做,但是不强求;
可以 (MAY) 和 可选 (OPTIONAL) :选择性高一点,在这个文档内,此词语使用较少;

参见:RFC 2119

1、总览

PSR-4 描述了从文件路径中 自动加载 类的规范。 它拥有非常好的兼容性,并且可以在任何自动加载规范中使用, 包括 PSR-0。 PSR-4 规范也描述了放置 autoload 文件(就是我们经常引入的 vendor/autoload.php)的位置。

2、规范

1、术语「class」指的是类(classes)、接口(interfaces)、特征(traits)和其他类似的结构。

2、全限定类名具有以下形式:

\<NamespaceName>(\<SubNamespaceNames>)*\<ClassName>

  • 全限定类名必须拥有一个顶级命名空间名称,也称为供应商命名空间(vendor namespace)。
  • 全限定类名可以有一个或者多个子命名空间名称。
  • 全限定类名必须有一个最终的类名(我想意思应该是你不能这样 \<NamespaceName>(\<SubNamespaceNames>)*\ 来表示一个完整的类)。
  • 下划线在全限定类名中没有任何特殊含义(在 PSR-0 中下划是有含义的)。
  • 全限定类名可以是任意大小写字母的组合。
  • 所有类名的引用必须区分大小写。

3、全限定类名的加载过程

  • 在全限定的类名(一个「命名空间前缀」)中,一个或多个前导命名空间和子命名空间组成的连续命名空间, 不包括前导命名空间的分隔符,至少对应一个「根目录」。
  • 「命名空间前缀」后面的相邻子命名空间与根目录下的目录名称相对应(且必须区分大小写), 其中命名空间的分隔符表示目录分隔符。
  • 最终的类名与以.php 结尾的文件名保持一致,这个文件的名字必须和最终的类名相匹配( 意思就是如果类名是 FooController,那么这个类所在的文件名必须是 FooController.php)。

4、自动加载文件禁止抛出异常,禁止出现任何级别的错误,也不建议有返回值。

3、范例

下表显示了与给定的全限定类名、命名空间前缀和根目录相对应的文件的路径。

Fully Qualified Class Name    Namespace Prefix   Base Directory           Resulting File Path
\Acme\Log\Writer\File_Writer  Acme\Log\Writer    ./acme-log-writer/lib/   ./acme-log-writer/lib/File_Writer.php
\Aura\Web\Response\Status     Aura\Web           /path/to/aura-web/src/   /path/to/aura-web/src/Response/Status.php
\Symfony\Core\Request         Symfony\Core       ./vendor/Symfony/Core/   ./vendor/Symfony/Core/Request.php
\Zend\Acl                     Zend               /usr/includes/Zend/      /usr/includes/Zend/Acl.php

想要了解一个符合规范的自动加载器的实现可以查看示例文件。示例中的自动加载器禁止被视为规范的一部分,它随时都可能发生改变。

PSR-4 自动加载规范 - 示例文档

PSR-4 的实现示例

下面的示例说明了符合 PSR-4 的代码:

闭包示例

<?php
/**
 * 一个具体项目实现的示例。
 *
 * 在注册自动加载函数后,
 * 下面这行代码将引发程序尝试从 /path/to/project/src/Baz/Qux.php 加载 \Foo\Bar\Baz\Qux 类:
 *
 *      new \Foo\Bar\Baz\Qux;
 *
 * @param string $class 完全标准的类名。
 * @return void
 */
spl_autoload_register(function ($class) {

    // 具体项目的命名空间前缀
    $prefix = 'Foo\\Bar\\';

    // 命名空间前缀对应的基础目录
    $base_dir = __DIR__ . '/src/';

    // 该类使用了此命名空间前缀?
    $len = strlen($prefix);
    if (strncmp($prefix, $class, $len) !== 0) {
        // 否,交给下一个已注册的自动加载函数
        return;
    }

    // 获取相对类名
    $relative_class = substr($class, $len);

    // 命名空间前缀替换为基础目录,
    // 将相对类名中命名空间分隔符替换为目录分隔符,
    // 附加 .php
    $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';

    // 如果文件存在,加载它
    if (file_exists($file)) {
        require $file;
    }
});

类示例

以下是一个可处理多命名空间的类实现示例:

<?php
namespace Example;

/**
 * 一个多用途的示例实现,包括了允许多个基本目录用于单个命名空间前缀的可选功能
 *
 * 下述示例给出了一个 foo-bar 类包,系统中路径结构如下……
 *
 *     /path/to/packages/foo-bar/
 *         src/
 *             Baz.php             # Foo\Bar\Baz
 *             Qux/
 *                 Quux.php        # Foo\Bar\Qux\Quux
 *         tests/
 *             BazTest.php         # Foo\Bar\BazTest
 *             Qux/
 *                 QuuxTest.php    # Foo\Bar\Qux\QuuxTest
 *
 * ……添加路径到  \Foo\Bar\  命名空间前缀的类文件中
 * 如下所示:
 *
 *      <?php
 *      // 实例化加载器
 *      $loader = new \Example\Psr4AutoloaderClass;
 *
 *      // 注册加载器
 *      $loader->register();
 *
 *      // 为命名空间前缀注册基本路径
 *      $loader->addNamespace('Foo\Bar', '/path/to/packages/foo-bar/src');
 *      $loader->addNamespace('Foo\Bar', '/path/to/packages/foo-bar/tests');
 *
 * 下述语句会让自动加载器尝试从 
 * /path/to/packages/foo-bar/src/Qux/Quux.php 
 * 中加载  \Foo\Bar\Qux\Quux 类
 *
 *      <?php
 *      new \Foo\Bar\Qux\Quux;
 *
 *  下述语句会让自动加载器尝试从 
 *   /path/to/packages/foo-bar/tests/Qux/QuuxTest.php
 * 中加载 \Foo\Bar\Qux\QuuxTest 类:
 *
 *      <?php
 *      new \Foo\Bar\Qux\QuuxTest;
 */
class Psr4AutoloaderClass
{
    /**
     * 关联数组,键名为命名空间前缀,键值为一个基本目录数组。
     *
     * @var array
     */
    protected $prefixes = array();

    /**
     * 通过 SPL 自动加载器栈注册加载器
     *
     * @return void
     */
    public function register()
    {
        spl_autoload_register(array($this, 'loadClass'));
    }

    /**
     * 为命名空间前缀添加一个基本目录
     *
     * @param string $prefix 命名空间前缀。
     * @param string $base_dir 命名空间下类文件的基本目录
     * @param bool $prepend 如果为真,预先将基本目录入栈而不是后续追加;这将使得它会被首先搜索到。
     * @return void
     */
    public function addNamespace($prefix, $base_dir, $prepend = false)
    {
        // 规范化命名空间前缀
        $prefix = trim($prefix, '\\') . '\\';

        // 规范化尾部文件分隔符
        $base_dir = rtrim($base_dir, DIRECTORY_SEPARATOR) . '/';

        // 初始化命名空间前缀数组
        if (isset($this->prefixes[$prefix]) === false) {
            $this->prefixes[$prefix] = array();
        }

        // 保留命名空间前缀的基本目录
        if ($prepend) {
            array_unshift($this->prefixes[$prefix], $base_dir);
        } else {
            array_push($this->prefixes[$prefix], $base_dir);
        }
    }

    /**
     * 加载给定类名的类文件
     *
     * @param string $class 合法类名
     * @return mixed 成功时为已映射文件名,失败则为 false
     */
    public function loadClass($class)
    {
        // 当前命名空间前缀
        $prefix = $class;

        // 通过完整的命名空间类名反向映射文件名
        while (false !== $pos = strrpos($prefix, '\\')) {

            // 在前缀中保留命名空间分隔符
            $prefix = substr($class, 0, $pos + 1);

            // 其余的是相关类名
            $relative_class = substr($class, $pos + 1);

            // 尝试为前缀和相关类加载映射文件
            $mapped_file = $this->loadMappedFile($prefix, $relative_class);
            if ($mapped_file) {
                return $mapped_file;
            }

            // 删除 strrpos() 下一次迭代的尾部命名空间分隔符
            $prefix = rtrim($prefix, '\\');
        }

        // 找不到映射文件
        return false;
    }

    /**
     * 为命名空间前缀和相关类加载映射文件。
     *
     * @param string $prefix 命名空间前缀
     * @param string $relative_class 相关类
     * @return mixed Boolean 无映射文件则为false,否则加载映射文件
     */
    protected function loadMappedFile($prefix, $relative_class)
    {
        // 命名空间前缀是否存在任何基本目录
        if (isset($this->prefixes[$prefix]) === false) {
            return false;
        }

        // 通过基本目录查找命名空间前缀
        foreach ($this->prefixes[$prefix] as $base_dir) {

            // 用基本目录替换命名空间前缀
            // 用目录分隔符替换命名空间分隔符
            // 给相关的类名增加 .php 后缀
            $file = $base_dir
                  . str_replace('\\', '/', $relative_class)
                  . '.php';

            // 如果映射文件存在,则引入
            if ($this->requireFile($file)) {
                // 搞定了
                return $file;
            }
        }

        // 找不到
        return false;
    }

    /**
     * 如果文件存在从系统中引入进来
     *
     * @param string $file 引入文件
     * @return bool 文件存在则 true 否则 false
     */
    protected function requireFile($file)
    {
        if (file_exists($file)) {
            require $file;
            return true;
        }
        return false;
    }
}

单元测试

以下示例是上述类加载器的单元测试方式之一:

<?php
namespace Example\Tests;

class MockPsr4AutoloaderClass extends Psr4AutoloaderClass
{
    protected $files = array();

    public function setFiles(array $files)
    {
        $this->files = $files;
    }

    protected function requireFile($file)
    {
        return in_array($file, $this->files);
    }
}

class Psr4AutoloaderClassTest extends \PHPUnit\Framework\TestCase
{
    protected $loader;

    protected function setUp(): void
    {
        $this->loader = new MockPsr4AutoloaderClass;

        $this->loader->setFiles(array(
            '/vendor/foo.bar/src/ClassName.php',
            '/vendor/foo.bar/src/DoomClassName.php',
            '/vendor/foo.bar/tests/ClassNameTest.php',
            '/vendor/foo.bardoom/src/ClassName.php',
            '/vendor/foo.bar.baz.dib/src/ClassName.php',
            '/vendor/foo.bar.baz.dib.zim.gir/src/ClassName.php',
        ));

        $this->loader->addNamespace(
            'Foo\Bar',
            '/vendor/foo.bar/src'
        );

        $this->loader->addNamespace(
            'Foo\Bar',
            '/vendor/foo.bar/tests'
        );

        $this->loader->addNamespace(
            'Foo\BarDoom',
            '/vendor/foo.bardoom/src'
        );

        $this->loader->addNamespace(
            'Foo\Bar\Baz\Dib',
            '/vendor/foo.bar.baz.dib/src'
        );

        $this->loader->addNamespace(
            'Foo\Bar\Baz\Dib\Zim\Gir',
            '/vendor/foo.bar.baz.dib.zim.gir/src'
        );
    }

    public function testExistingFile()
    {
        $actual = $this->loader->loadClass('Foo\Bar\ClassName');
        $expect = '/vendor/foo.bar/src/ClassName.php';
        $this->assertSame($expect, $actual);

        $actual = $this->loader->loadClass('Foo\Bar\ClassNameTest');
        $expect = '/vendor/foo.bar/tests/ClassNameTest.php';
        $this->assertSame($expect, $actual);
    }

    public function testMissingFile()
    {
        $actual = $this->loader->loadClass('No_Vendor\No_Package\NoClass');
        $this->assertFalse($actual);
    }

    public function testDeepFile()
    {
        $actual = $this->loader->loadClass('Foo\Bar\Baz\Dib\Zim\Gir\ClassName');
        $expect = '/vendor/foo.bar.baz.dib.zim.gir/src/ClassName.php';
        $this->assertSame($expect, $actual);
    }

    public function testConfusion()
    {
        $actual = $this->loader->loadClass('Foo\Bar\DoomClassName');
        $expect = '/vendor/foo.bar/src/DoomClassName.php';
        $this->assertSame($expect, $actual);

        $actual = $this->loader->loadClass('Foo\BarDoom\ClassName');
        $expect = '/vendor/foo.bardoom/src/ClassName.php';
        $this->assertSame($expect, $actual);
    }
}

PSR-4 自动加载规范 - 说明文档

PSR-4 说明文档

1、概述

PSR-4 是为了给可交互的 PHP 自动加载器指定一个将命名空间映射到文件系统的规则, 并且可以与其他 SPL 注册的自动加载器共存。PSR-4 不是 PSR-0 的替代品,而是对它的补充。

2、为什么需要它?

PSR-0 的发展史

在 PHP 5.2 之前,PSR-0 的类命名标准和自动加载标准是以被广泛使用的 Horde/PEAR 约定为准。 这个约定里要求将所有的 PHP 类文件放在一个主目录中,并使用下划线连接的字符串来表示命名空间,如下所示:

/path/to/src/
    VendorFoo/
        Bar/
            Baz.php     # VendorFoo_Bar_Baz
    VendorDib/
        Zim/
            Gir.php     # VendorDib_Zim_Gir

随着 PHP 5.3 的发布以及新命名空间的广泛使用,现在 PSR-0 已经可以使用 Horde/PEAR 约定的下划线表示法或者新命名空间表示法。 它让下划线表示法作为新命名空间表示法的一个过渡,从而得到更好的兼容。

/path/to/src/
    VendorFoo/
        Bar/
            Baz.php     # VendorFoo_Bar_Baz
    VendorDib/
        Zim/
            Gir.php     # VendorDib_Zim_Gir
    Irk_Operation/
        Impending_Doom/
            V1.php
            V2.php      # Irk_Operation\Impending_Doom\V2

这种目录结构很大程度影响了 PEAR 安装器将源文件从 PEAR 包中迁移到一个主目录中。

因 Composer 而来

在 Composer 中,包资源不再拷贝到某个单一的全局目录。从它们安装的位置引用它们,不需要移动。 这就意味着使用 Composer 时 PHP 资源不像 PEAR 一样有「单一主目录」。取而代之的是多个目录;每个项目的每个包都在单独目录中。

为了符合 PSR-0 的需要,导致每个 Composer 包都类似下面这样:

vendor/
    vendor_name/
        package_name/
            src/
                Vendor_Name/
                    Package_Name/
                        ClassName.php       # Vendor_Name\Package_Name\ClassName
            tests/
                Vendor_Name/
                    Package_Name/
                        ClassNameTest.php   # Vendor_Name\Package_Name\ClassNameTest

「src」和「tests」目录必须包含开发商和包目录名。这是遵守 PSR-0 带来的结构。

许多人认为这种结构比需要的更深更重复。这一提议建议一个额外的或替代性的 PSR 将会更有益,所以我们有了类似以下的包结构:

vendor/
    vendor_name/
        package_name/
            src/
                ClassName.php       # Vendor_Name\Package_Name\ClassName
            tests/
                ClassNameTest.php   # Vendor_Name\Package_Name\ClassNameTest

这就需要将最初称为「基于包的自动加载」实现(对应于传统的「类 - 文件自动加载」)。

面向 - 包的自动加载

通过扩展或修订 PSR-0 实现面向 - 包的自动加载非常困难,因为 PSR-0 不允许修改类名路径之间的任何部分。 这意味着实现面向 - 包的自动加载要比 PSR-0 复杂的多,但是从另一方面来讲,它将使扩展包更加简洁。

在一开始的时候,以下规则是建议的:

  1. 实现者必须使用两个以上的命名空间层级:一个 vendor 名,和该 vendor 内的包名。 (这两个顶级名称组合被简称为 vendor-package 或 vendor-package namespace。)
  2. 实现者必须允许 vendor-package namespace 与完全限定类名的其余部分之间的路径中缀。
  3. vendor-package namespace 可以映射到任意目录。完全限定类名的其余部分, 必须映射命名空间名称到同名目录,类名必须映射到 .php 结尾的同名文件。

注意这意味着结束了在类名中下划线作为目录分隔符的做法。有人可能认为下划线应该被遵从因为它们出现在 PSR-0 规范当中, 但是在该文档中它们作为 PHP 5.2 或者更旧的版本的伪命名空间过渡的做法,所以此处删除他们也是可以接受的。

3、范围(Scope)

3.1 目标

  • 保留实现者必须使用两个以上的命名空间层级的 PSR-0 规则:一个 vendor 名,和该 vendor 内的 包名。
  • 允许 vendor-package namespace 与完全限定类名的其余部分的路径中缀。
  • 允许 vendor-package namespace 可以映射到任何目录,也可能是多个目录。
  • 结束遵从类名中下划线作为目录分隔符的做法。

3.2 非目标

  • 为非类资源提供通用的转换规则

4、方案选择

4.1 被选中的方案

本方案保留了 PSR-0 关键特性,同时消除了更深层次的目录结构。此外,指定了一些附加规则,使得操作起来更明确。

尽管不涉及目录映射,最终草案还是规定了自动加载器应该如何处理错误。具体来说,它禁止抛出异常和错误,主要有这两方面考虑:

  1. PHP 中自动加载器设计是可堆叠的,如果一个自动加载器不能加载,则其他的仍有机会继续加载。 若有其中一个自动加载器发生错误此过程将不会进行下去;
  2. class_exists()interface_exists() 允许『在尝试自动加载后仍然找不到类』的存在, 一个用例是:若自动加载器抛出异常将使得 class_exists() 不可用,从互操作性的角度来看这是无法接受的。 自动加载器在找不到类的情况下最好通过日志记录提供附加的调试信息,日志可以使用 PSR-3 兼容日志记录或类似的方案。

优点:

较浅的目录结构;
文件位置更加固定;
不再使用类名中下划线作为目录分隔符;
更明确的互操作性实现

缺点:

不能像 PSR-0 仅仅通过类名就能确定它在文件系统的具体位置 (这种 “类 - 到 - 文件” 约定继承自 Horde/PEAR)。

4.2 替代方案:只使用 PSR-0

保留 PSR-0 虽然很合理,但确实给我们留下了相对较深的目录结构。

优点:

无需改变任何人的习惯或者实现方式

缺点:

相对较深的目录结构
类名中的下划线被识别为目录分隔符

4.3 替代方案:拆分自动加载以及转换

Beau Simensen 跟其他人建议,转换算法可以从自动加载提案中分离出来以便转换规则可以被其他的提案引用。 在完成分离它们的工作之后,会进行民意调查跟一些相关讨论。通过后,合并版本(即带转换规则的自动加载提案)会被显示为首选项。

优点:

转换规则可以被其他提案引用

缺点:

不符合某民意调查的受访者跟合作者的意愿(就是要修改老代码,有些人怕麻烦)

4.4 替代方案:使用更多命令式和叙事性语言

在多个 +1 选民听到他们支持这个想法但未同意(或理解)该提案的措辞后,赞助商撤回了第二次投票后, 有一段时间,投票通过的提案得到了扩展。更大的叙事和更有必要的语言。少数参与者谴责这种方法。 一段时间后,Beau Simensen 开始进行实验性修订,着眼于 PSR-0 。编辑和赞助商赞成采用这种更简洁的方法, 并指导现在正在考虑的版本,由 Paul M. Jones 编写并为许多人做出贡献。

与 PHP 5.3.2 及更低版本的兼容性说明

5.3.3 之前的 PHP 版本不会删除前导命名空间分隔符,因此需要注意实施过程。无法删除前导命名空间分隔符可能会导致意外行为。

5、参与人员

5.1 编辑

Paul M. Jones, Solar/Aura

5.2 赞助者

Phil Sturgeon, PyroCMS (Coordinator)
Larry Garfield, Drupal

5.3 贡献者

Andreas Hennings
Bernhard Schussek
Beau Simensen
Donald Gilbert
Mike van Riel
Paul Dragoonis
Too many others to name and count

6、投票情况

入选投票 : https://groups.google.com/d/msg/php-fig/_LYBgfcEoFE/ZwFTvVTIl4AJ

接受投票:

  • 第一次尝试: https://groups.google.com/forum/#!topic/php-fig/Ua46E344_Ls, presented prior to new workflow; aborted due to accidental proposal modification
  • 第二次尝试: https://groups.google.com/forum/#!topic/php-fig/NWfyAeF7Psk, cancelled at the discretion of the sponsor https://groups.google.com/forum/#!topic/php-fig/t4mW2TQF7iE
  • 第三次尝试:暂时没有信息

7、相关链接

Autoloader, round 4
POLL: Autoloader: Split or Combined?
PSR-X autoloader spec: Loopholes, ambiguities
Autoloader: Combine Proposals?
Package-Oriented Autoloader, Round 2
Autoloader: looking again at namespace
DISCUSSION: Package-Oriented Autoloader - vote against
VOTE: Package-Oriented Autoloader
Proposal: Package-Oriented Autoloader
Towards a Package Oriented Autoloader
List of Alternative PSR-4 Proposals
Summary of [post-Acceptance Vote pull] PSR-4 discussions

PSR-6 缓存接口规范

缓存是提升应用性能的常用手段,为框架中最通用的功能,每个框架也都推出专属的、功能多样的缓存库。 这些差别使得开发人员不得不学习多种系统,而很多可能是他们并不需要的功能。 此外,缓存库的开发者同样面临着一个窘境,是只支持有限数量的几个框架还是创建一堆庞大的适配器类。

一个通用的缓存系统接口可以解决掉这些问题。库和框架的开发人员能够知道缓存系统会按照他们所预期的方式工作, 缓存系统的开发人员只需要实现单一的接口,而不用去开发各种各样的适配器。

本文件中的 必须,不得,需要,应,不应,应该,不应该,推荐,可能 和 可选 等能愿动词按照 RFC 2119 中的描述进行解释。

目标

本 PSR 的目标是:创建一套通用的接口规范,能够让开发人员整合到现有框架和系统,而不需要去开发框架专属的适配器类。

定义

调用类库 (Calling Library) - 调用者,使用缓存服务的类库,这个类库调用缓存服务, 调用的是此缓存接口规范的具体「实现类库」,调用者不需要知道任何「缓存服务」的具体实现。

实现类库 (Implementing Library) - 此类库是对「缓存接口规范」的具体实现,封装起来的缓存服务,供「调用类库」使用。 实现类库 必须 提供 PHP 类来实现 Cache\CacheItemPoolInterfaceCache\CacheItemInterface 接口。 实现类库 必须 支持最小的如下描述的 TTL 功能,秒级别的精准度。

生存时间值 (TTL - Time To Live) - 定义了缓存可以存活的时间,以秒为单位的整数值。

过期时间 (Expiration) - 定义准确的过期时间点,一般为缓存存储发生的时间点加上 TTL 时间值, 也可以指定一个 DateTime 对象。假如一个缓存项的 TTL 设置为 300 秒,保存于 1:30:00 ,那么缓存项的过期时间为 1:35:00。 实现类库 可以 让缓存项提前过期,但是 必须 在到达过期时间时立即把缓存项标示为过期。 如果调用类库在保存一个缓存项的时候未设置「过期时间」、或者设置了 null 作为过期时间(或者 TTL 设置为 null), 实现类库 可以 使用默认自行配置的一个时间。如果没有默认时间,实现类库 必须把存储时间当做 永久性 存储, 或者按照底层驱动能支持的最长时间作为保持时间。

键 (KEY) - 长度大于 1 的字串,用作缓存项在缓存系统里的唯一标识符。 实现类库 必须 支持「键」规则 A-Z, a-z, 0-9, _, 和 . 任何顺序的 UTF-8 编码,长度小于 64 位。 实现类库 可以 支持更多的编码或者更长的长度,不过 必须 支持至少以上指定 的编码和长度。实现类库可自行实现对「键」的转义,但是 必须 保证能够无损的返回「键」字串。 以下的字串作为系统保留: {}()/\@:,一定不可 作为「键」的命名支持。

命中 (Hit) - 一个缓存的命中,指的是当调用类库使用「键」在请求一个缓存项的时候, 在缓存池里能找到对应的缓存项,并且此缓存项还未过期,并且此数据不会因为任何原因出现错误。 调用类库 应该 确保先验证下 isHit() 有命中后才调用 get() 获取数据。

未命中 (Miss) - 一个缓存未命中,是完全的上面描述的「命中」的相反。 指的是当调用类库使用「键」在请求一个缓存项的时候,在缓存池里未能找到对应的缓存项,或者此缓存项已经过期, 或者此数据因为任何原因出现错误。一个过期的缓存项,必须 被当做 未命中 来对待。

延迟 (Deferred) - 一个延迟的缓存,指的是这个缓存项可能不会立刻被存储到物理缓存池里。 一个缓存池对象 可以 对一个指定延迟的缓存项进行延迟存储,这样做的好处是可以利用一些缓存服务器提供的批量插入功能。 缓存池 必须 能对所有延迟缓存最终能持久化,并且不会丢失。可以 在调用类库还未发起保存请求之前就做持久化。 当调用类库调用 commit() 方法时,所有的延迟缓存都 必须 做持久化。 实现类库 可以 自行决定使用什么逻辑来触发数据持久化,如对象的 析构方法 (destructor) 内、 调用 save() 时持久化、倒计时保存或者触及最大数量时保存等。 当请求一个延迟缓存项时,必须 返回一个延迟,未持久化的缓存项对象。

数据

实现类库必须支持序列化 PHP 的所有数据类型,包含以下类型:

  • Strings - PHP 兼容编码中任意长度的字符串。
  • Integers - PHP 支持的所有大小的整数,最大为 64 位有符号整数。
  • Floats - 有符号的浮点值。
  • Boolean - True 和 False 。
  • Null - 实际的空值。
  • Arrays - 可索引,关联,任意深度的数组。
  • Object - 所有对象支持无损的序列化和反序列化,比如 $o == unserialize(serialize($o))。 对象可以使用 PHP 的原生序列化接口, __sleep()__wakeup() 等魔术方法,或者其他可用的类似语法。

所有数据传入实现类库 必须 准确返回传入的值,并且返回变量的类型一致。非法示例:传入 (int) 5 返回 (string) 5。 实现类库 可以 使用 PHP 中 serialize()/unserialize() 方法进行序列化和反序列化,当然也不强制要求这样做。 兼容这些的一个基本准线就是接受对象类型。

如果由于某些技术原因不能准确保存,实现类库 必须 返回缓存未命中,而不是损坏的数据。

主要概念

  • 缓存池 Pool

缓存池包含缓存系统里所有缓存数据的集合。缓存池逻辑上是所有缓存项存储的仓库,所有存储进去的数据, 都能从缓存池里取出来,所有的对缓存的操作,都发生在缓存池子里。

  • 缓存项 Items

一条缓存项在缓存池里代表了一对「键 / 值」对应的数据,「键」被视为每一个缓存项主键, 是缓存项的唯一标识符,必须 是不可变更的,当然,「值」可以 任意变更。

错误处理

缓存对应用性能起着至关重要的作用,但是,无论在任何情况下,缓存 一定不可 作为应用程序不可或缺的核心功能。 缓存系统里的错误 一定不可 导致应用程序故障,所以,实现类库 一定不可 抛出任何除了此接口规范定义的以外的异常, 并且 必须 捕捉包括底层存储驱动抛出的异常,不让其冒泡至超出缓存系统内。

实现类库 应该 对此类错误进行记录,或者以任何形式通知管理员。

调用类库发起删除缓存项的请求,或者清空整个缓冲池子的请求,「键」不存在的话 必须 不能当成是有错误发生。 后置条件是一样的,如果取数据时,「键」不存在的话 必须 不能当成是有错误发生。

接口

  • CacheItemInterface

CacheItemInterface 定义了缓存系统里的一个缓存项。每一个缓存项 必须 有一个「键」与之相关联, 此「键」通常是通过 Cache\CacheItemPoolInterface 来设置。

Cache\CacheItemInterface 对象把缓存项的存储进行了封装, 每一个 Cache\CacheItemInterface 由一个 Cache\CacheItemPoolInterface 对象生成, CacheItemPoolInterface 负责一些必须的设置,并且给对象设置具有 唯一性 的「键」。

Cache\CacheItemInterface 对象 必须 能够存储和取出任何类型的,在「数据」章节定义的 PHP 数值。

调用类库 一定不可 擅自初始化「CacheItemInterface」对象, 「缓存项」只能使用「CacheItemPoolInterface」对象的 getItem() 方法来获取。 调用类库 一定不可 假设由一个实现类库创建的「缓存项」能被另一个实现类库完全兼容。

<?php

namespace Psr\Cache;

/**
 * CacheItemInterface 定了缓存系统里对缓存项操作的接口
 */
interface CacheItemInterface
{
    /**
     * 返回当前缓存项的「键」
     *
     * 「键」由实现类库来加载,并且高层的调用者(如:CacheItemPoolInterface)
     * **应该** 能使用此方法来获取到「键」的信息。
     *
     * @return string
     *  当前缓存项的「键」
     */
    public function getKey();

    /**
     * 凭借此缓存项的「键」从缓存系统里面取出缓存项。
     *
     * 取出的数据 **必须** 跟使用 `set()` 存进去的数据是一模一样的。
     *
     * 如果 `isHit()` 返回 false 的话,此方法必须返回 `null`,
     * 需要注意的是 `null` 本来就是一个合法的缓存数据,
     * 所以你 **应该** 使用 `isHit()` 方法来辨别到底是"返回 null 数据" 还是 "缓存里没有此数据"
     *
     * @return mixed
     *   此缓存项的「键」对应的「值」,如果找不到的话,返回 `null`
     */
    public function get();

    /**
     * 确认缓存项的检查是否命中。
     *
     * 注意: 调用此方法和调用 `get()` 时 **一定不可** 有先后顺序之分。
     *
     * @return bool
     * 如果缓冲池里有命中的话,返回 `true`,反之返回 `false`
     */
    public function isHit();

    /**
     * 为此缓存项设置「值」。
     *
     * 参数 $value 可以是所有能被 PHP 序列化的数据,
     * 序列化的逻辑需要在实现类库里书写。
     *
     * @param mixed $value
     * 将被存储的可序列化的数据。
     *
     * @return static
     * 返回当前对象。
     */
    public function set($value);

    /**
     * 设置缓存项的准确过期时间点。
     *
     * @param \DateTimeInterface|null $expiration
     * 过期的准确时间点,过了这个时间点后,缓存项就 **必须** 被认为是过期了的。
     * 如果明确的传参 `null` 的话,**可以** 使用一个默认的时间。
     * 如果没有设置的话,缓存 **应该** 存储到底层实现的最大允许时间。
     *
     * @return static
     * 返回当前对象
     */
    public function expiresAt($expiration);

    /**
     * 设置缓存项的过期时间
     *
     * @param int|\DateInterval|null $time
     * 以秒为单位的过期时长,过了这段时间后,缓存项就 **必须** 被认为是过期了的。
     * 如果明确的传参 `null` 的话,**可以** 使用一个默认的时间。
     * 如果没有设置的话,缓存 **应该** 存储到底层实现的最大允许时间。
     *
     * @return static
     * 返回当前对象
     */
    public function expiresAfter($time);

}
  • CacheItemPoolInterface

Cache\CacheItemPoolInterface 的主要目的是从调用类库接收「键」,然后返回对应的 Cache\CacheItemInterface 对象。

此接口也是作为主要的,与整个缓存集合交互的方式。所有的配置和初始化由实现类库自行实现。

<?php

namespace Psr\Cache;

/**
 * CacheItemPoolInterface 生成 CacheItemInterface 对象
 */
interface CacheItemPoolInterface
{
    /**
     * 返回「键」对应的一个缓存项。
     *
     * 此方法 **必须** 返回一个 CacheItemInterface 对象,
     * 即使是找不到对应的缓存项也 **一定不可** 返回 `null`。
     *
     * @param string $key
     * 用来搜索缓存项的「键」。
     *
     * @throws InvalidArgumentException
     *   如果 $key 不是合法的值,\Psr\Cache\InvalidArgumentException 异常会被抛出。
     *
     * @return CacheItemInterface
     *  对应的缓存项。
     */
    public function getItem($key);

    /**
     * 返回一个可供遍历的缓存项集合。
     *
     * @param string[] $keys
     * 由一个或者多个「键」组成的数组。
     *
     * @throws InvalidArgumentException
     * 如果 $keys 里面有哪个「键」不是合法,\Psr\Cache\InvalidArgumentException 异常会被抛出。
     *   
     *
     * @return array|\Traversable
     * 返回一个可供遍历的缓存项集合,集合里每个元素的标识符由「键」组成,即使即使是找不到对的缓存项,
     * 也要返回一个「CacheItemInterface」对象到对应的「键」中。
     * 如果传参的数组为空,也需要返回一个空的可遍历的集合。
     */
    public function getItems(array $keys = array());

    /**
     * 检查缓存系统中是否有「键」对应的缓存项。
     *
     * 注意: 此方法应该调用 `CacheItemInterface::isHit()` 来做检查操作,而不是 `CacheItemInterface::get()`
     *
     * @param string $key
     * 用来搜索缓存项的「键」。
     *
     * @throws InvalidArgumentException
     *   如果 $key 不是合法的值,\Psr\Cache\InvalidArgumentException 异常会被抛出。
     *
     * @return bool
     *   如果存在「键」对应的缓存项即返回 true,否则 false
     */
    public function hasItem($key);

    /**
     * 清空缓冲池
     *
     * @return bool
     *   成功返回 true,有错误发生返回 false
     */
    public function clear();

    /**
     * 从缓冲池里移除某个缓存项
     *
     * @param string $key
     *   用来搜索缓存项的「键」。
     *
     * @throws InvalidArgumentException
     * 如果 $key 不是合法的值,\Psr\Cache\InvalidArgumentException 异常会被抛出。
     *
     * @return bool
     *   成功返回 true,有错误发生返回 false
     */
    public function deleteItem($key);

    /**
     * 从缓冲池里移除多个缓存项
     *
     * @param string[] $keys
     * 由一个或者多个「键」组成的数组。
     * 
     * @throws InvalidArgumentException
     * 如果 $keys 里面有哪个「键」不是合法,\Psr\Cache\InvalidArgumentException 异常会被抛出。
     *
     * @return bool
     * 成功返回 true,有错误发生返回 false
     */
    public function deleteItems(array $keys);

    /**
     * 立刻为「CacheItemInterface」对象做数据持久化。
     *
     * @param CacheItemInterface $item
     * 将要被存储的缓存项
     *
     * @return bool
     * 成功返回 true,有错误发生返回 false
     */
    public function save(CacheItemInterface $item);

    /**
     * 稍后为「CacheItemInterface」对象做数据持久化。
     *
     * @param CacheItemInterface $item
     * 将要被存储的缓存项
     *
     * @return bool
     * 成功返回 true,有错误发生返回 false
     */
    public function saveDeferred(CacheItemInterface $item);

    /**
     * 提交所有的正在队列里等待的请求到数据持久层,配合 `saveDeferred()` 使用
     *
     * @return bool
     * 成功返回 true,有错误发生返回 false
     */
    public function commit();
}
  • CacheException

异常接口用于严重错误发生的时候,包括但不限于缓存设置错误,例如:无法连接到缓存服务器、提供了无效的凭证。

库抛出的任何异常都必须实现此接口。

<?php

namespace Psr\Cache;

/**
 * 异常接口,针对库抛出的所有异常。
 */
interface CacheException
{
}
  • InvalidArgumentException
<?php

namespace Psr\Cache;

/**
 * 无效缓存参数的异常接口。
 *
 * 任何时候,一个无效参数传递到方法时,
 * 必须抛出一个实现了 Psr\Cache\InvalidArgumentException 的异常类。
 */
interface InvalidArgumentException extends CacheException
{
}

PSR-6 缓存接口规范 - 说明文档

1、 概述

使用缓存是一种常用的提高性能的方法,并适用于任何项目,这使得缓存库成为许多框架和库最常见的特性之一。 最后导致了许多库都有自己的缓存库,并且具有不同级别的功能。这些差异导致开发人员不得不学习多个缓存系统, 而他们所需要的功能可能在有的系统里并没有提供。此外,缓存库本身的开发人员只能选择要么支持少量框架,要么创建大量的适配器类。

2、 为什么有必要?

通用的缓存接口会解决这些问题。库和框架开发者可以期望缓存系统能正常工作, 与此同时,缓存系统的开发者只用实现一部分接口而不是做一大堆适配工作。

而且,这里的实现也是为了方便未来扩展,它提供了许多本质上不同但是却又兼容 API 的实现, 而且也为后面 PSRs 规范或者特定的实现提供了清晰的路径规划。

正方:

一个标准的缓存接口能提供独立的库来让我们轻松缓存中间数据;他们可以简单的依赖和使用这些标准接口而不用关心具体的实现细节。
由多个项目共享的常见开发的缓存系统,即使他们扩展了这个接口,也比单独开发的实现要健壮

反方:

任何接口标准化会被认为扼杀了未来创新,被认为不应该这样实现。但是我们相信缓存是一个足够商业化的问题场景,缓存接口在这里提供的扩展能力降低了任何潜在的停滞风险。

3、 范围

3.1 目标

一种通用的底层和中间级缓存需求接口。
一种清晰的机制,用于扩展规范以支持高级功能,包括将来的 PSRs 或单个实现。 此机制必须允许多个独立扩展而不会发生冲突。

3.2 非目标

与所有现有缓存的实现体系结构兼容。
像命名空间或标记这样由少数用户使用的高级缓存特性。

4、 方法

4.1 选择的方法

该规范采用『存储模型』或『数据映射』模型进行缓存,而不是传统可『可过期键 - 值』模型。 主要原因是灵活性。简单的键 / 值模型更加难以扩展。

这里的模型要求用 CacheItem 对象和 Pool 对象,CacheItem 对象表示缓存条目,Pool 对象是缓存数据给定缓存。 从池中检索项目,交互并返回到项目。有时候有些冗长,但是它提供了一个良好、稳健、灵活的缓存方法, 尤其是在缓存比简单的保存在字符串更复杂的情况下。

大多数方法名称是根据成员项目和其他流行的非成员系统调查中的通用实践和方法名来选择的。

优点:

灵活并且可扩展
允许在不违反接口的情况下实现大量的变化
不会将对象构造函数的隐式暴露为伪接口

缺点:

比简单的方法更冗长

示例:

下面是一些常用的使用模式。这些是非规范的,但是可以说明一些设计决策的应用。

/**
 * 获取可用控件列表。
 *
 * 在这种情况下,我们假设小部件列表很少改动 
 * 列表一直缓存到显式清除为止。
 */
function get_widget_list()
{
    $pool = get_cache_pool('widgets');
    $item = $pool->getItem('widget_list');
    if (!$item->isHit()) {
        $value = compute_expensive_widget_list();
        $item->set($value);
        $pool->save($item);
    }
    return $item->get();
}
/**
 * 可用控件缓存列表。
 *
 * 在这种情况下,我们假设已经计算了一个小部件列表,
 * 缓存它,无论缓存的是什么。
 */
function save_widget_list($list)
{
    $pool = get_cache_pool('widgets');
    $item = $pool->getItem('widget_list');
    $item->set($list);
    $pool->save($item);
}
/**
 * 清除缓存小部件列表。
 *
 * 在这种情况下,我们只想从缓存中删除小部件。
 * 我们不在意他是否已被设置;POST的条件是『不再设置』 。
 */
function clear_widget_list()
{
    $pool = get_cache_pool('widgets');
    $pool->deleteItems(['widget_list']);
}
/**
 * 清除所有的小部件。
 *
 * 在这种情况下,我们只想清空池中所有的小部件。 
 * 应用中其他的池可能不会受到影响。
 */
function clear_widget_cache()
{
    $pool = get_cache_pool('widgets');
    $pool->clear();
}
/**
 * 加载小部件
 *
 * 我们想要获取一个小部件的列表,其中一些是缓存 一些不是。
 * 这里假设从缓存中加载比在非缓存加载机制中更快。
 *
 * 在这种情况下, 假设窗口小部件需要经常更改因此我们仅设置缓存的时间为一小时 (3600 秒)。
 * 我们也将新缓存的对象返回到池中。
 *
 * 还需要注意在实际实现中还需要对小部件窗口进行多次加载操作,
 * 但是这与本次演示无关。
 */
function load_widgets(array $ids)
{
    $pool = get_cache_pool('widgets');
    $keys = array_map(function($id) { return 'widget.' . $id; }, $ids);
    $items = $pool->getItems($keys);

    $widgets = array();
    foreach ($items as $key => $item) {
        if ($item->isHit()) {
            $value = $item->get();
        } else {
            $value = expensive_widget_load($id);
            $item->set($value);
            $item->expiresAfter(3600);
            $pool->saveDeferred($item, true);
        }
        $widget[$value->id()] = $value;
    }
    $pool->commit(); // 如果没有延期的项目这里无操作。

    return $widgets;
}
/**
 * 这个示例反应了此规范未包含的功能,
 * 但是显示为如何通过扩展来实现添加此类功能的示例。
 */

interface TaggablePoolInterface extends Psr\Cache\CachePoolInterface
{
    /**
     * 只清除池中指定标记的项目。
     */
    clearByTag($tag);
}

interface TaggableItemInterface extends Psr\Cache\CacheItemInterface
{
    public function setTags(array $tags);
}

/**
 * 标记缓存小部件。
 */
function set_widget(TaggablePoolInterface $pool, Widget $widget)
{
    $key = 'widget.' . $widget->id();
    $item = $pool->getItem($key);

    $item->setTags($widget->tags());
    $item->set($widget);
    $pool->save($item);
}

4.2 替代方案: “弱项” 方法

许多早起的草案采用了一种更简单的「带过期时间的键值对」的方法,同时也被称之为 「弱项」 方法。 在这种模型中,「缓存项」 对象只是一个不能使用的数组方法对象。用户可以直接实例化它,然后将它们扔进缓存池。 虽然这种方法更为常见,但它有效的防止了缓存项的任何有意义的扩展。 它有效的使缓存项的构造函数成为隐式接口的一部分,从而严重的降低了缓存项在实际灵活应用中的可扩展能力。

在 2013 年的一次调研中,大多数参与者都表现出明显的偏好,如果不大传统的 「强项」存储库方法更为健壮, 那么它将被采用作为未来发展的方向。

正方:

更加传统的方法.

反方:

较差的扩展及灵活性.

4.3 选择: “Naked value” 方法

一些早期的缓存规范讨论建议跳过 “缓存项” 概念,而只是读取 / 写入要缓存的原始值。 尽管更简单,但需要指出的是,这使得无法分辨出缓存未命中与已缓存的原始值之间的区别。 也就是说,如果缓存查找返回 NULL,则无法判断是否没有缓存的值或 NULL 是否为已缓存的值。 (在很多情况下,NULL 是已缓存的值。)

我们审查过的最健壮的缓存实现 - 尤其是 Stash 缓存库和 Drupal 使用的本地缓存系统 - 至少在 get 上使用某种结构化对象, 以避免混淆未命中值和标记值。 Based on that prior experience FIG decided that a naked value on get was impossible. 根据先前的经验,FIG 认为在 get 上 Naked value 是不可能的。

4.4 选择: ArrayAccess Pool

有人建议让 Pool 实现 ArrayAccess,这将允许缓存获取 / 设置操作使用数组语法。 由于应用有限而被拒绝,该方法的灵活性有限(使用默认控制信息进行简单的获取和设置就可以实现),如果需要,将特定实现包含为附加组件很简单。

5、 参与者

5.1 文档

Larry Garfield

5.2 赞助商

Paul Dragoonis, PPI Framework (Coordinator)
Robert Hafner, Stash

6、 投票详情

Acceptance vote on the mailing list

7、 链接

Note: Order descending chronologically.

Survey of existing cache implementations, by @dragoonis
Strong vs. Weak informal poll, by @Crell
Implementation details informal poll, by @Crell

8、 其他

8.1 在 expiresAt () 中处理不正确的 DateTime 值

在接口中 CacheItemInterface::expiresAt() 方法的 $expiration 参数中我们未做类型提示, 但在文档块中将其指定为 \DateTimeInterface。 目的是允许使用 \DateTime\DateTimeImmutable 对象。 但是,在 PHP 5.5 中添加了 \DateTimeInterface\DateTimeImmutable,并且作者选择不在规范上强加 PHP 5.5 的严格语法要求。

尽管如此,实现者必须只接受 \DateTimeInterface 或兼容的类型(例如 \DateTime\DateTimeImmutable), 就好像该方法已做类型提示一样。 (请注意,在不同的语言版本之间,类型化参数的差异规则可能会有所不同。)

模拟失败的类型检查在 PHP 不同版本之间会有所不同,因此不建议这样做。 相反,实现者应该抛出 \Psr\Cache\InvalidArgumentException 的实例。

建议使用以下示例代码,以便对 expiresAt()方法执行类型检查:

class ExpiresAtInvalidParameterException implements Psr\Cache\InvalidArgumentException {}

// ...

if (! (
        null === $expiration
        || $expiration instanceof \DateTime
        || $expiration instanceof \DateTimeInterface
)) {
    throw new ExpiresAtInvalidParameterException(sprintf(
        'Argument 1 passed to %s::expiresAt() must be an instance of DateTime or DateTimeImmutable; %s given',
        get_class($this),
        is_object($expiration) ? get_class($expiration) : gettype($expiration)
    ));
}

PSR-7 HTTP 消息接口规范

HTTP 消息接口

此文档描述了 RFC 7230 和 RFC 7231 HTTP 消息传递的接口,还有 RFC 3986 里对 HTTP 消息的 URIs 使用。

HTTP 消息是 Web 技术发展的基础。浏览器或 HTTP 客户端如 curl 生成发送 HTTP 请求消息到 Web 服务器, Web 服务器响应 HTTP 请求。服务端的代码接受 HTTP 请求消息后返回 HTTP 响应消息。

通常 HTTP 消息对于终端用户来说是不可见的,但是作为 Web 开发者,我们需要知道 HTTP 机制, 如何发起、构建、取用还有操纵 HTTP 消息,知道这些原理,以助我们更好的完成开发任务, 无论这个任务是发起一个 HTTP 请求,或者处理传入的请求。

每一个 HTTP 请求都有专属的格式:

POST /path HTTP/1.1
Host: example.com

foo=bar&baz=bat

按照顺序,第一行的各个字段意义为: HTTP 请求方法、请求的目标地址(通常是一个绝对路径的 URI 或者路径),HTTP 协议。 接下来是 HTTP 头信息,在这个例子中:目的主机。接下来是空行,然后是消息内容。

HTTP 返回消息有类似的结构:

HTTP/1.1 200 OK
Content-Type: text/plain

这是返回的消息内容

按照顺序,第一行为状态行,包括 HTTP 协议版本,HTTP 状态码,描述文本。和 HTTP 请求类似的,接下来是 HTTP 头信息, 在这个例子中:内容类型。接下来是空行,然后是消息内容。

此文档探讨的是 HTTP 请求消息接口,和构建 HTTP 消息需要的元素数据定义。

本文件中的 必须,不得,需要,应,不应,应该,不应该,推荐,可能 和 可选 等能愿动词按照 RFC 2119 中的描述进行解释。

参考文献

RFC 2119
RFC 3986
RFC 7230
RFC 7231 

1、详细描述

1.1 消息

一个 HTTP 消息是指来自于客户端到服务端的请求或者服务端到客户端的响应。 以下这两个文档分别为 HTTP 的消息接口做了详细定义 Psr\Http\Message\RequestInterfacePsr\Http\Message\ResponseInterface

Psr\Http\Message\RequestInterfacePsr\Http\Message\ResponseInterface 继承于 Psr\Http\Message\MessageInterface 。 当接口 Psr\Http\Message\MessageInterface 可能被直接实现的时候, 实现者应该实现 Psr\Http\Message\RequestInterface 接口和 Psr\Http\Message\ResponseInterface 接口。

从这里开始,当描述这些接口时,命名空间 Psr\Http\Message 将会被省略。

1.2 HTTP 请求头信息

大小写不敏感的字段名字

HTTP 消息包含大小写不敏感头信息。使用 MessageInterface 接口来设置和获取头信息,大小写不敏感的定义在于, 如果你设置了一个 Foo 的头信息,foo 的值会被重写,你也可以通过 foo 来拿到 FoO 头对应的值。

$message = $message->withHeader('foo', 'bar');

echo $message->getHeaderLine('foo');
// 输出: bar

echo $message->getHeaderLine('FOO');
// 输出: bar

$message = $message->withHeader('fOO', 'baz');
echo $message->getHeaderLine('foo');
// 输出: baz

虽然头信息可以用大小写不敏感的方式取出,但是接口实现类 必须 保持自己的大小写规范,特别是用 getHeaders() 方法输出的内容。

因为一些非标准的 HTTP 应用程序,可能会依赖于大小写敏感的头信息,所以在此我们把主宰 HTTP 大小写的权利开放出来,以适用不同的场景。

对应多条数组的头信息

为了适用一个 HTTP 「键」可以对应多条数据的情况,我们使用字符串配合数组来实现,你可以从一个 MessageInterface 取出数组或字符串, 使用 getHeaderLine($name) 方法可以获取通过逗号分割的不区分大小写的字符串形式的所有值。 也可以通过 getHeader($name) 获取数组形式头信息的所有值。

$message = $message
    ->withHeader('foo', 'bar')
    ->withAddedHeader('foo', 'baz');

$header = $message->getHeaderLine('foo');
// $header 包含: 'bar, baz'

$header = $message->getHeader('foo');
// ['bar', 'baz']

注意:并不是所有的头信息都可以适用逗号分割(例如 Set-Cookie),当处理这种头信息时候, MessageInterace 的继承类 应该 使用 getHeader($name) 方法来获取这种多值的情况。

主机信息

在请求中,Host 头信息通常和 URI 的 host 信息,还有建立起 TCP 连接使用的 Host 信息一致。 然而,HTTP 标准规范允许主机 host 信息与其他两个不一样。

在构建请求的时候,如果 host 头信息未提供的话,实现类库 必须 尝试着从 URI 中提取 host 信息。

RequestInterface::withUri() 会默认的,从传参的 UriInterface 实例中提取 host ,并替代请求中原有的 host 信息。

你可以提供传参第二个参数为 true 来保证返回的消息实例中,原有的 host 头信息不会被替代掉。

以下表格说明了当 withUri() 的第二个参数被设置为 true 的时,返回的消息实例中调用 getHeaderLine('Host') 方法会返回的内容:

请求 Host 头信息 1 请求 URI 中的 Host 信息 2 传参进去 URI 的 Host 3 结果
‘’ ‘’ ‘’ ‘’
‘’ foo.com ‘’ foo.com
‘’ foo.com bar.com foo.com
foo.com ‘’ bar.com foo.com
foo.com bar.com baz.com foo.com
  • 1 当前请求的 Host 头信息。
  • 2 当前请求 URI 中的 Host 信息。
  • 3 通过 withUri() 传参进入的 URI 中的 host 信息。

1.3 数据流

HTTP 消息包含开始的一行、头信息、还有消息的内容。HTTP 的消息内容有时候可以很小,有时候确是非常巨大。 尝试使用字符串的形式来展示消息内容,会消耗大量的内存,使用数据流的形式来读取消息可以解决此问题。 StreamInterface 接口用来隐藏具体的数据流读写实现。在一些情况下,消息类型的读取方式为字符串是能容许的, 可以使用 php://memory 或者 php://temp

StreamInterface 暴露出来几个接口,这些接口允许你读取、写入,还有高效的遍历内容。

数据流使用这个三个接口来阐明对他们的操作能力:isReadable()isWritable()isSeekable()。 这些方法可以让数据流的操作者得知数据流能否能提供他们想要的功能。

每一个数据流的实例,都会有多种功能:可以只读、可以只写、可以读和写,可以随机读取,可以按顺序读取等。

最终,StreamInterface 定义了一个 __toString() 的方法,用来一次性以字符串的形式输出所有消息内容。

与请求和响应的接口不同的是,StreamInterface 并不强调不可修改性。因为在 PHP 的实现内,基本上没有办法保证不可修改性, 因为指针的指向,内容的变更等状态,都是不可控的。作为读取者,可以调用只读的方法来返回数据流, 以最大程度上保证数据流的不可修改性。使用者要时刻明确的知道数据流的可修改性,建议把数据流附加到消息实例中,来强迫不可修改的特性。

1.4 请求目标和 URI

根据 RFC7230,请求消息包含「请求目标」做为请求行的第二个段落。请求目标可以是以下形式之一:

  • 原始形式 ,由路径和查询字符串(如果存在)组成;这通常被称为相对 URL。通过 TCP 传输的消息通常是原始形式; scheme 和认证数据通常仅通过 CGI 变量存在。
  • 绝对形式 ,包括 scheme 、认证数据(「[user-info@] host [:port]」,其中括号中的项是可选的), 路径(如果存在),查询字符串(如果存在)。这通常被称为绝对 URI,并且是 RFC 3986 中详细说明的唯一指定 URI 的形式。 这个形式通常在向 HTTP 代理发出请求时使用。
  • 认证形式 ,只包含认证信息。通常仅用于从 HTTP 客户端和代理服务器之间建立连接请求时使用。
  • 星号形式 ,仅由字符串 * 组成,并与 OPTIONS 方法一起使用,以确定 Web 服务器的性能。

除了这些请求目标之外,通常还有一个不同于请求目标的「有效 URL」。有效 URL 不在 HTTP 消息中传输, 但它用于确定发出请求的协议(Http 或 Https)、端口和主机名。

有效 URL 由 UriInterface 接口表示。UriInterface 是 RFC 3986 (主要用例)中指定的 HTTP 和 HTTPS URI 的模型。 该接口提供了与各种 URI 部分交互的方法,这将消除重复解析 URI 的需要。还定义了一个 __toString() 方法, 用于将建模的 URI 转换为其字符串表示形式。

当使用 getRequestTarget() 方法检索请求目标时,默认情况下此方法将使用 URI 对象并提取所有必要的组件来构建 原始形式。 原始形式 是迄今为止最常见的请求目标。

如果用户希望使用其他三种形式中,或者如果想要显式覆盖请求目标,则可以使用 withRequestTarget() 来实现。

调用此方法不会影响 URI,因为 URI 是从 getUri() 返回的。

例如,用户可能想要向服务器发起一个星号形式的请求:

$request = $request
    ->withMethod('OPTIONS')
    ->withRequestTarget('*')
    ->withUri(new Uri('https://example.org/'));

这个示例最终可能会导致 HTTP 请求类似下例:

OPTIONS * HTTP/1.1

但是 HTTP 客户端将能够使用有效的 URL (来自 getUri() )来确定协议、主机名和 TCP 端口号。

一个 HTTP 客户端 必须 忽略 Uri::getPath()Uri::getQuery() 的值,而是用 getRequestTarget() 返回的值,默认为连接前面两个值。

选择未实现上面四种请求目标形式的客户端,必须 依然使用 getRequestTarget()。 这些客户端 必须 拒绝它们不支持的请求目标,并且 不得 依赖于 getUri() 的值。

RequestInterface 提供了检索请求目标或用提供的请求目标创建一个新实例的方法。 默认情况下,如果实例中没有专门组合请求目标, getRequestTarget() 将会返回组合 URI 的原始形式(如果没有组成 URI 则返回/)。 withRequestTarget($requestTarget) 使用指定的请求目标创建一个新实例, 从而允许开发人员创建表示其他三个请求目标形式(绝对形式、认证形式和星号形式)。 使用时,组合的 URI 实例仍然可以使用,特别是在客户端中,它可以用于创建与服务器的连接。

1.5 服务端请求

RequestInterface 提供了 HTTP 请求消息的通常表示形式。但是,由于服务器端环境的性质,服务器端请求需要额外的处理。 服务器端处理需要考虑通用网关接口( CGI ),更具体地说,需要考虑 PHP 通过其服务器 API ( SAPI )对 CGI 的抽象和扩展。 PHP 通过超级全局变量提供了关于输入编组的简化,例如:

  • $_COOKIE ,反序列化了 HTTP cookie,并提供了简化的访问方式。
  • $_GET ,反序列化了查询字符串并提供了简化的访问方式。
  • $_POST ,对通过 urlencode 编码提交的 HTTP POST 信息进化反序列化并提供了简化的访问方式;通常可以认为是解析消息体的结果。
  • $_FILES ,关于文件上传的元数据反序列化结果。
  • $_SERVER ,提供了 CGI/SAPI 环境变量的访问,这些变量通常包括请求方法、请求 scheme、请求 URI 和报头。

ServerRequestInterface 继承于 RequestInterface,提供围绕这些超全局变量的抽象访问。 这种做法有助于减少开发人员对超全局的耦合,鼓励对代码的测试,并提升了测试人员对相应代码的测试能力。

服务器请求提供了一个附加的属性,「attributes」,以便于开发人员可以根据应用程序的特定规则 (例如路径匹配、scheme 匹配、主机匹配等)自检、分解和匹配请求。这样,服务器请求还可以在多段请求逻辑中进行消息传递。

1.6 文件上传

ServerRequestInterface 指定了一种在规范化结构中检索上传文件树的方法,每个叶子都是一个 UploadedFileInterface 的实例。

超全局变量 $_FILES 在处理文件数组式的时候存在一些众所周知的问题。 具体而言,页面的表单里有多个 input 框,name 属性是 files[],然后提交文件,PHP 的 $_FILES 变量形式如下:

array(
    'files' => array(
        'name' => array(
            0 => 'file0.txt',
            1 => 'file1.html',
        ),
        'type' => array(
            0 => 'text/plain',
            1 => 'text/html',
        ),
        /* 等等其他属性 */
    ),
)

而不是预期的:

array(
    'files' => array(
        0 => array(
            'name' => 'file0.txt',
            'type' => 'text/plain',
            /* 等等其他属性 */
        ),
        1 => array(
            'name' => 'file1.html',
            'type' => 'text/html',
            /* 等等其他属性 */
        ),
    ),
)

这样造成的结果是开发人员必须知道这种语言实现细节,并为之编写特定的代码。

另外,如果发生以下情况, $_FILES 会是空数组:

  • HTTP 方法不是 POST。
  • 单元测试的时候。
  • 在非 SAPI 环境下运行的时候,比如 ReactPHP。

在这些情况下,数据需要以不同的方式获取。比如:

  • 进程可以解析消息体来发现上传的文件。这种情况下,实现方式可以选择不将上传文件写入文件系统, 而是将它们包装在流中以减少内存、I/O 和存储开销。
  • 在单元测试的场景下,开发人员需要能够对文件上桩或模仿的方式来验证和检查不同场景的情况。

getUploadedFiles() 将为开发者提供规范化的结构。实现方式的返回定义是:

  • 聚合上传文件的所有信息,并填充 Psr\Http\Message\UploadedFileInterface 实例。
  • 重新创建提交的树结构,相应位置的叶结点都是一个适当的 Psr\Http\Message\UploadedFileInterface 实例。

引用的树结构 应该 模仿提交的文件结构。

在最简单的示例中,这可能是单个被命名的提交表单元素:

<input type="file" name="avatar" />

在这种情况下,$_FILES 的结构如下:

array(
    'avatar' => array(
        'tmp_name' => 'phpUxcOty',
        'name' => 'my-avatar.png',
        'size' => 90996,
        'type' => 'image/png',
        'error' => 0,
    ),
)

getUploadedFiles() 返回的规范化形式将是:

array(
    'avatar' => /* UploadedFileInterface 实例 */
)

input 名称是一种数组表示形式的情况:

<input type="file" name="my-form[details][avatar]" />

$_FILES 最终看下来像是这样的:

array(
    'my-form' => array(
        'details' => array(
            'avatar' => array(
                'tmp_name' => 'phpUxcOty',
                'name' => 'my-avatar.png',
                'size' => 90996,
                'type' => 'image/png',
                'error' => 0,
            ),
        ),
    ),
)

getUploadedFiles() 的返回结果 应该 是:

array(
    'my-form' => array(
        'details' => array(
            'avatar' => /* UploadedFileInterface 实例 */
        ),
    ),
)

在某些情况下,可以指定文件的 input 为一个数组:

Upload an avatar: <input type="file" name="my-form[details][avatars][]" />
Upload an avatar: <input type="file" name="my-form[details][avatars][]" />

(例如,JavaScript 控件可能会产生额外的文件上传输入,以允许一次上传多个文件。)

这种情况下,其实现 必须 按给定的索引聚合所有上传文件的信息。因为这种情况下的 $_FILES 偏离了正常结构:

array(
    'my-form' => array(
        'details' => array(
            'avatars' => array(
                'tmp_name' => array(
                    0 => '...',
                    1 => '...',
                    2 => '...',
                ),
                'name' => array(
                    0 => '...',
                    1 => '...',
                    2 => '...',
                ),
                'size' => array(
                    0 => '...',
                    1 => '...',
                    2 => '...',
                ),
                'type' => array(
                    0 => '...',
                    1 => '...',
                    2 => '...',
                ),
                'error' => array(
                    0 => '...',
                    1 => '...',
                    2 => '...',
                ),
            ),
        ),
    ),
)

上面的 $_FILES 将对应于 getUploadedFiles() 返回的如下结构:

array(
    'my-form' => array(
        'details' => array(
            'avatars' => array(
                0 => /* UploadedFileInterface 实例 */,
                1 => /* UploadedFileInterface 实例 */,
                2 => /* UploadedFileInterface 实例 */,
            ),
        ),
    ),
)

开发人员可以用以下形式访问嵌套数组的索引 1:

$request->getUploadedFiles()['my-form']['details']['avatars'][1];

因为上传的文件数据是派生的(派生于 $_FILES 或请求体),所以接口还有一个设置方法 withUploadedFiles(),允许修改其内容。

在原始示例的情形下,接口调用者的代码可能如下所示:

$file0 = $request->getUploadedFiles()['files'][0];
$file1 = $request->getUploadedFiles()['files'][1];

printf(
    "Received the files %s and %s",
    $file0->getClientFilename(),
    $file1->getClientFilename()
);

// "Received the files file0.txt and file1.html"

这个设计方案还考虑到实现方案可以在非 SAPI 环境中运行。 As such, UploadedFileInterface provides methods for ensuring operations will work regardless of environment. 特别是:

  • moveTo($targetPath) 用来做为一个安全且推荐的代替在临时上传文件上调用 move_uploaded_file() 的方法。 实现将根据环境检查正确的操作。
  • getStream() 将会返回一个 StreamInterface 实例。在非 SAPI 环境中, 提出的一种可能性是将单个上传文件解析为 php://temp 流而不是直接解析到文件;在这种情况下,不存在上传文件。 因此,无论环境如何,getStream() 都可以保证工作。

例如:

// 移动文件至上传目录
$filename = sprintf(
    '%s.%s',
    create_uuid(),
    pathinfo($file0->getClientFilename(), PATHINFO_EXTENSION)
);
$file0->moveTo(DATA_DIR . '/' . $filename);

// 将文件流式传输至 Amazon S3。
// 假设 $s3wrapper 是一个将写入 S3 的 PHP 流,而 Psr7StreamWrapper 是一个将 StreamInterface 作为 PHP StreamWrapper 进行装饰的类。
$stream = new Psr7StreamWrapper($file1->getStream());
stream_copy_to_stream($stream, $s3wrapper);

2、扩展包

上面讨论的接口和类库已经整合成为扩展包:psr/http-message。

3、接口

3.1 Psr\Http\Message\MessageInterface

<?php
namespace Psr\Http\Message;

/**
 * 
 * HTTP 消息包括客户端向服务器发起的「请求」和服务器端返回给客户端的「响应」。
 * 此接口定义了他们通用的方法。
 * 
 * HTTP 消息是被视为无法修改的,所有能修改状态的方法,都 **必须** 有一套
 * 机制,在内部保持好原有的内容,然后把修改状态后的信息返回。
 *
 * @see http://www.ietf.org/rfc/rfc7230.txt
 * @see http://www.ietf.org/rfc/rfc7231.txt
 */
interface MessageInterface
{
    /**
     * 获取字符串形式的 HTTP 协议版本信息。
     *
     * 字符串 **必须** 包含 HTTP 版本数字(如:「1.1」, 「1.0」)。
     *
     * @return string HTTP 协议版本
     */
    public function getProtocolVersion();

    /**
     * 返回指定 HTTP 版本号的消息实例。
     *
     * 传参的版本号只 **必须** 包含 HTTP 版本数字,如:"1.1", "1.0"。
     *
     * 此方法在实现的时候,**必须** 保留原有的不可修改的 HTTP 消息对象,然后返回
     * 一个新的带有传参进去的 HTTP 版本的实例
     *
     * @param string $version HTTP 版本信息
     * @return self
     */
    public function withProtocolVersion($version);

    /**
     * 获取所有的报头信息
     *
     * 返回的二维数组中,第一维数组的「键」代表单条报头信息的名字,「值」是
     * 以数组形式返回的,见以下实例:
     *
     *     // 把「值」的数据当成字串打印出来
     *     foreach ($message->getHeaders() as $name => $values) {
     *         echo $name . ': ' . implode(', ', $values);
     *     }
     *
     *     // 迭代的循环二维数组
     *     foreach ($message->getHeaders() as $name => $values) {
     *         foreach ($values as $value) {
     *             header(sprintf('%s: %s', $name, $value), false);
     *         }
     *     }
     *
     * 虽然报头信息是没有大小写之分,但是使用 `getHeaders()` 会返回保留了原本
     * 大小写形式的内容。
     *
     * @return string[][] 返回一个两维数组,第一维数组的「键」 **必须** 为单条报头信息的
     *     名称,对应的是由字串组成的数组,请注意,对应的「值」 **必须** 是数组形式的。
     */
    public function getHeaders();

    /**
     * 检查是否报头信息中包含有此名称的值,不区分大小写
     *
     * @param string $name 不区分大小写的报头信息名称
     * @return bool 找到返回 true,未找到返回 false
     */
    public function hasHeader($name);

    /**
     * 根据给定的名称,获取一条报头信息,不区分大小写,以数组形式返回
     *
     * 此方法以数组形式返回对应名称的报头信息。
     *
     * 如果没有对应的报头信息,**必须** 返回一个空数组。
     *
     * @param string $name 不区分大小写的报头字段名称。
     * @return string[] 返回报头信息中,对应名称的,由字符串组成的数组值,如果没有对应
     *     的内容,**必须** 返回空数组。
     */
    public function getHeader($name);

    /**
     * 根据给定的名称,获取一条报头信息,不区分大小写,以逗号分隔的形式返回
     * 
     * 此方法返回所有对应的报头信息,并将其使用逗号分隔的方法拼接起来。
     *
     * 注意:不是所有的报头信息都可使用逗号分隔的方法来拼接,对于那些报头信息,请使用
     * `getHeader()` 方法来获取。
     * 
     * 如果没有对应的报头信息,此方法 **必须** 返回一个空字符串。
     *
     * @param string $name 不区分大小写的报头字段名称。
     * @return string 返回报头信息中,对应名称的,由逗号分隔组成的字串,如果没有对应
     *     的内容,**必须** 返回空字符串。
     */
    public function getHeaderLine($name);

    /**
     * 返回替换指定报头信息「键/值」对的消息实例。
     *
     * 虽然报头信息是不区分大小写的,但是此方法必须保留其传参时的大小写状态,并能够在
     * 调用 `getHeaders()` 的时候被取出。
     *
     * 此方法在实现的时候,**必须** 保留原有的不可修改的 HTTP 消息对象,然后返回
     * 一个更新后带有传参进去报头信息的实例
     *
     * @param string $name 不区分大小写的报头字段名称。
     * @param string|string[] $value 报头信息或报头信息数组。
     * @return self
     * @throws \InvalidArgumentException 无效的报头字段或报头信息时抛出
     */
    public function withHeader($name, $value);

    /**
     * 返回一个报头信息增量的 HTTP 消息实例。
     *
     * 原有的报头信息会被保留,新的值会作为增量加上,如果报头信息不存在的话,字段会被加上。
     *
     * 此方法在实现的时候,**必须** 保留原有的不可修改的 HTTP 消息对象,然后返回
     * 一个新的修改过的 HTTP 消息实例。
     *
     * @param string $name 不区分大小写的报头字段名称。
     * @param string|string[] $value 报头信息或报头信息数组。
     * @return self
     * @throws \InvalidArgumentException 报头字段名称非法时会被抛出。
     * @throws \InvalidArgumentException 报头头信息的值非法的时候会被抛出。
     */
    public function withAddedHeader($name, $value);

    /**
     * 返回被移除掉指定报头信息的 HTTP 消息实例。
     *
     * 报头信息字段在解析的时候,**必须** 保证是不区分大小写的。
     *
     * 此方法在实现的时候,**必须** 保留原有的不可修改的 HTTP 消息对象,然后返回
     * 一个新的修改过的 HTTP 消息实例。
     *
     * @param string $name 不区分大小写的头部字段名称。
     * @return self
     */
    public function withoutHeader($name);

    /**
     * 获取 HTTP 消息的内容。
     *
     * @return StreamInterface 以数据流的形式返回。
     */
    public function getBody();

    /**
     * 返回指定内容的 HTTP 消息实例。
     *
     * 内容 **必须** 是 `StreamInterface` 接口的实例。
     *
     * 此方法在实现的时候,**必须** 保留原有的不可修改的 HTTP 消息对象,然后返回
     * 一个新的修改过的 HTTP 消息实例。
     *
     * @param StreamInterface $body 数据流形式的内容。
     * @return self
     * @throws \InvalidArgumentException 当消息内容不正确的时候抛出。
     */
    public function withBody(StreamInterface $body);
}

3.2 Psr\Http\Message\RequestInterface

<?php
namespace Psr\Http\Message;

/**
 * 代表客户端向服务器发起请求的 HTTP 消息对象。
 *
 * 根据 HTTP 规范,此接口包含以下属性:
 *
 * - HTTP 协议版本号
 * - HTTP 请求方法
 * - URI
 * - 报头信息
 * - 消息内容
 *
 * 在构造 HTTP 请求对象的时候,如果没有提供 Host 信息,
 * 实现类库 **必须** 从给出的 URI 中去提取 Host 信息。
 *
 * HTTP 请求是被视为无法修改的,所有能修改状态的方法,都 **必须** 有一套机制,在内部保
 * 持好原有的内容,然后把修改状态后的新的 HTTP 请求实例返回。
 */
interface RequestInterface extends MessageInterface
{
    /**
     * 获取消息的请求目标。
     * 
     * 获取消息的请求目标的使用场景,可能是在客户端,也可能是在服务器端,也可能是在指定信息的时候
     * (参阅下方的 `withRequestTarget()`)。
     * 
     * 在大部分情况下,此方法会返回组合 URI 的原始形式,除非被指定过(参阅下方的 `withRequestTarget()`)。
     *
     * 如果没有可用的 URI,并且没有设置过请求目标,此方法 **必须** 返回 「/」。
     *
     * @return string
     */
    public function getRequestTarget();

    /**
     * 返回一个指定目标的请求实例。
     * 
     * 如果请求需要非原始形式的请求目标——例如指定绝对形式、认证形式或星号形式——则此方法
     * 可用于创建指定请求目标的实例。
     *
     * 此方法在实现的时候,**必须** 保留原有的不可修改的 HTTP 请求实例,然后返回
     * 一个新的修改过的 HTTP 请求实例。
     *
     * @see [http://tools.ietf.org/html/rfc7230#section-2.7](http://tools.ietf.org/html/rfc7230#section-2.7) 
     * (关于请求目标的各种允许的格式)
     * 
     * @param mixed $requestTarget
     * @return self
     */
    public function withRequestTarget($requestTarget);

    /**
     * 获取当前请求使用的 HTTP 方法
     *
     * @return string HTTP 方法字符串
     */
    public function getMethod();

    /**
     * 返回更改了请求方法的消息实例。
     *
     * 虽然,在大部分情况下,HTTP 请求方法都是使用大写字母来标示的,但是,实现类库 **不应该**
     * 修改用户传参的大小格式。
     * 
     * 此方法在实现的时候,**必须** 保留原有的不可修改的 HTTP 请求实例,然后返回
     * 一个新的修改过的 HTTP 请求实例。
     *
     * @param string $method 大小写敏感的方法名
     * @return self
     * @throws \InvalidArgumentException 当非法的 HTTP 方法名传入时会抛出异常。
     */
    public function withMethod($method);

    /**
     * 获取 URI 实例。
     *
     * 此方法 **必须** 返回 `UriInterface` 的 URI 实例。
     *
     * @see http://tools.ietf.org/html/rfc3986#section-4.3
     * @return UriInterface 返回与当前请求相关的 `UriInterface` 类型的 URI 实例。
     */
    public function getUri();

    /**
     * 返回修改了 URI 的消息实例。
     *
     * 当传入的 URI 包含有 HOST 信息时,此方法 **必须** 更新 HOST 信息。如果 URI 
     * 实例没有附带 HOST 信息,任何之前存在的 HOST 信息 **必须** 作为候补,应用
     * 更改到返回的消息实例里。
     * 
     * 你可以通过传入第二个参数来,来干预方法的处理,当 `$preserveHost` 设置为 `true` 
     * 的时候,会保留原来的 HOST 信息。当 `$preserveHost` 设置为 `true` 时,此方法
     * 会如下处理 HOST 信息:
     * 
     * - 如果 HOST 信息不存在或为空,并且新 URI 包含 HOST 信息,则此方法 **必须** 更新返回请求中的 HOST 信息。
     * - 如果 HOST 信息不存在或为空,并且新 URI 不包含 HOST 信息,则此方法 **不得** 更新返回请求中的 HOST 信息。
     * - 如果HOST 信息存在且不为空,则此方法 **不得** 更新返回请求中的 HOST 信息。
     * 
     * 此方法在实现的时候,**必须** 保留原有的不可修改的 HTTP 请求实例,然后返回
     * 一个新的修改过的 HTTP 请求实例。
     *
     * @see http://tools.ietf.org/html/rfc3986#section-4.3
     * @param UriInterface $uri `UriInterface` 新的 URI 实例
     * @param bool $preserveHost 是否保留原有的 HOST 头信息
     * @return self
     */
    public function withUri(UriInterface $uri, $preserveHost = false);
}

3.2.1 Psr\Http\Message\ServerRequestInterface

<?php
namespace Psr\Http\Message;

/**
 * 表示服务器端接收到的 HTTP 请求。
 *
 * 根据 HTTP 规范,此接口包含以下属性:
 *
 * - HTTP 协议版本号
 * - HTTP 请求方法
 * - URI
 * - 报头信息
 * - 消息内容
 *
 * 此外,它封闭了从 CGI 和/或 PHP 环境变量,包括:
 *
 * - `$_SERVER` 中表示的值。
 * - 提供的任意 Cookie 信息(通常通过 `$_COOKIE` 获取)
 * - 查询字符串参数(通常通过 `$_GET` 获取,或者通过 `parse_str()` 解析)
 * - 如果存在的话,上传文件的信息(通常通过 `$_FILES` 获取)
 * - 反序列化的消息体参数(通常来自于 `$_POST`)
 *
 * `$_SERVER` 的值 **必须** 被视为不可变的,因为代表了请求时应用程序的状态;因此,没有允许修改的方法。
 * 其他值则提供了修改的方法,因为可以从 `$_SERVER` 或请求体中恢复,并且可能在应用程序中被处理
 * (比如可能根据内容类型对消息体参数进行反序列化)。
 *
 * 此外,这个接口要识别请求的扩展信息和匹配其他的参数。
 * (例如,通过 URI 进行路径匹配,解析 Cookie 值,反序列化非表单编码的消息体,报头中的用户名进行匹配认证)
 * 这些参数存储在「attributes」中。
 *
 * HTTP 请求是被视为无法修改的,所有能修改状态的方法,都 **必须** 有一套机制,在内部保
 * 持好原有的内容,然后把修改状态后的,新的 HTTP 请求实例返回。
 */
interface ServerRequestInterface extends RequestInterface
{
    /**
     * 返回服务器参数。
     *
     * 返回与请求环境相关的数据,通常从 PHP 的 `$_SERVER` 超全局变量中获取,但不是必然的。
     *
     * @return array
     */
    public function getServerParams();

    /**
     * 获取 Cookie 数据。
     *
     * 获取从客户端发往服务器的 Cookie 数据。
     *
     * 这个数据的结构 **必须** 和超全局变量 `$_COOKIE` 兼容。
     *
     * @return array
     */
    public function getCookieParams();

    /**
     * 返回具体指定 Cookie 的实例。
     *
     * 这个数据不是一定要来源于 `$_COOKIE`,但是 **必须** 与之结构兼容。通常在实例化时注入。
     *
     * 这个方法 **禁止** 更新实例中的 Cookie 报头和服务器参数中的相关值。
     *
     * 此方法在实现的时候,**必须** 保留原有的不可修改的 HTTP 消息实例,然后返回
     * 一个新的修改过的 HTTP 消息实例。
     * 
     * @param array $cookies 表示 Cookie 的键值对。
     * @return self
     */
    public function withCookieParams(array $cookies);

    /**
     * 获取查询字符串参数。
     *
     * 如果可以的话,返回反序列化的查询字符串参数。
     *
     * 注意:查询参数可能与 URI 或服务器参数不同步。如果你需要确保只获取原始值,则可能需要调用
     * `getUri()->getQuery()` 或服务器参数中的 `QUERY_STRING` 获取原始的查询字符串并自行解析。
     *
     * @return array
     */
    public function getQueryParams();

    /**
     * 返回具体指定查询字符串参数的实例。
     *
     * 这些值 **应该** 在传入请求的闭包中保持不变。它们 **可能** 在实例化的时候注入,
     * 例如来自 `$_GET` 或者其他一些值(例如 URI)中得到。如果是通过解析 URI 获取,则
     * 数据结构必须与 `parse_str()` 返回的内容兼容,以便处理查询参数、嵌套的代码可以复用。
     *
     * 设置查询字符串参数 **不得** 更改存储的 URI 和服务器参数中的值。
     * 
     * 此方法在实现的时候,**必须** 保留原有的不可修改的 HTTP 消息实例,然后返回
     * 一个新的修改过的 HTTP 消息实例。
     *
     * @param array $query 查询字符串参数数组,通常来源于 `$_GET`。
     * @return self
     */
    public function withQueryParams(array $query);

    /**
     * 获取规范化的上传文件数据。
     *
     * 这个方法会规范化返回的上传文件元数据树结构,每个叶子结点都是 `Psr\Http\Message\UploadedFileInterface` 实例。
     *
     * 这些值 **可能** 在实例化的时候从 `$_FILES` 或消息体中获取,或者通过 `withUploadedFiles()` 获取。
     *
     * @return array `UploadedFileInterface` 的实例数组;如果没有数据则必须返回一个空数组。
     */
    public function getUploadedFiles();

    /**
     * 返回使用指定的上传文件数据的新实例。
     *
     * 此方法在实现的时候,**必须** 保留原有的不可修改的 HTTP 消息实例,然后返回
     * 一个新的修改过的 HTTP 消息实例。
     *
     * @param array `UploadedFileInterface` 实例的树结构,类似于 `getUploadedFiles()` 的返回值。
     * @return self
     * @throws \InvalidArgumentException 如果提供无效的结构时抛出。
     */
    public function withUploadedFiles(array $uploadedFiles);

    /**
     * 获取请求消息体中的参数。
     *
     * 如果请求的 Content-Type 是 application/x-www-form-urlencoded 或 multipart/form-data 且请求方法是 POST,
     * 则此方法 **必须** 返回 $_POST 的内容。
     *
     * 如果是其他情况,此方法可能返回反序列化请求正文内容的任何结果;当解析返回返回的结构化内容时,潜在的类型 **必须**
     * 只能是数组或 `object` 类型。`null` 表示没有消息体内容。
     *
     * @return null|array|object 如果存在则返回反序列化消息体参数。一般是一个数组或 `object`。
     */
    public function getParsedBody();

    /**
     * 返回具有指定消息体参数的实例。
     *
     * **可能** 在实例化时注入。
     *
     * 如果请求的 Content-Type 是 application/x-www-form-urlencoded 或 multipart/form-data 且请求方法是 POST,
     * 则方法的参数只能是 $_POST。
     *
     * 数据不一定要来自 $_POST,但是 **必须** 是反序列化请求正文内容的结果。由于需要反序列化/解析返回的结构化数据,
     * 所以这个方法只接受数组、 `object` 类型和 `null`(如果没有可用的数据解析)。
     *
     * 例如,如果确定请求数据是一个 JSON,可以使用此方法创建具有反序列化参数的请求实例。
     *
     * 此方法在实现的时候,**必须** 保留原有的不可修改的 HTTP 消息实例,然后返回
     * 一个新的修改过的 HTTP 消息实例。
     *
     * @param null|array|object $data 反序列化的消息体数据,通常是数组或 `object`。
     * @return self
     * @throws \InvalidArgumentException 如果提供的数据类型不支持。
     */
    public function withParsedBody($data);

    /**
     * 获取从请求派生的属性。
     *
     * 请求「attributes」可用于从请求导出的任意参数:比如路径匹配操作的结果;解密 Cookie 的结果;
     * 反序列化非表单编码的消息体的结果;属性将是应用程序与请求特定的,并且可以是可变的。
     *
     * @return mixed[] 从请求派生的属性。
     */
    public function getAttributes();

    /**
     * 获取单个派生的请求属性。
     *
     * 获取 getAttributes() 中声明的某一个属性,如果不存在则返回提供的默认值。
     *
     * 这个方法不需要 hasAttribute 方法,因为允许在找不到指定属性的时候返回默认值。
     *
     * @see getAttributes()
     * @param string $name 属性名称。
     * @param mixed $default 如果属性不存在时返回的默认值。
     * @return mixed
     */
    public function getAttribute($name, $default = null);

    /**
     * 返回具有指定派生属性的实例。
     *
     * 此方法允许设置 getAttributes() 中声明的单个派生的请求属性。
     *
     * 此方法在实现的时候,**必须** 保留原有的不可修改的 HTTP 消息实例,然后返回
     * 一个新的修改过的 HTTP 消息实例。
     *
     * @see getAttributes()
     * @param string $name 属性名。
     * @param mixed $value 属性值。
     * @return self
     */
    public function withAttribute($name, $value);

    /**
     * 返回移除指定属性的实例。
     *
     * 此方法允许移除 getAttributes() 中声明的单个派生的请求属性。
     *
     * 此方法在实现的时候,**必须** 保留原有的不可修改的 HTTP 消息实例,然后返回
     * 一个新的修改过的 HTTP 消息实例。
     *
     * @see getAttributes()
     * @param string $name 属性名。
     * @return self
     */
    public function withoutAttribute($name);
}

3.3 Psr\Http\Message\ResponseInterface

<?php
namespace Psr\Http\Message;

/**
 * 表示服务器返回的响应消息。
 *
 * 根据 HTTP 规范,此接口包含以下各项的属性:
 *
 * - 协议版本
 * - 状态码和原因短语
 * - 报头
 * - 消息体
 * 
 * HTTP 响应是被视为无法修改的,所有能修改状态的方法,都 **必须** 有一套机制,在内部保
 * 持好原有的内容,然后把修改状态后的,新的 HTTP 响应实例返回。
 */
interface ResponseInterface extends MessageInterface
{
    /**
     * 获取响应状态码。
     *
     * 状态码是一个三位整数,用于理解请求。
     *
     * @return int 状态码。
     */
    public function getStatusCode();

    /**
     * 返回具有指定状态码和原因短语(可选)的实例。
     *
     * 如果未指定原因短语,实现代码 **可能** 选择 RFC7231 或 IANA 为状态码推荐的原因短语。
     *
     * 此方法在实现的时候,**必须** 保留原有的不可修改的 HTTP 消息实例,然后返回
     * 一个新的修改过的 HTTP 消息实例。
     *
     * @see http://tools.ietf.org/html/rfc7231#section-6
     * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
     * @param int $code 三位整数的状态码。
     * @param string $reasonPhrase 为状态码提供的原因短语;如果未提供,实现代码可以使用 HTTP 规范建议的默认代码。
     * @return self
     * @throws \InvalidArgumentException 如果传入无效的状态码,则抛出。
     */
    public function withStatus($code, $reasonPhrase = '');

    /**
     * 获取与响应状态码关联的响应原因短语。
     *
     * 因为原因短语不是响应状态行中的必需元素,所以原因短语 **可能** 是空。
     * 实现代码可以选择返回响应的状态代码的默认 RFC 7231 推荐原因短语(或 IANA HTTP 状态码注册表中列出的原因短语)。
     *
     * @see http://tools.ietf.org/html/rfc7231#section-6
     * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
     * @return string 原因短语;如果不存在,则 **必须** 返回空字符串。
     */
    public function getReasonPhrase();
}

3.4 Psr\Http\Message\StreamInterface

<?php
namespace Psr\Http\Message;

/**
 * 描述数据流。
 *
 * 通常,实例将包装PHP流; 此接口提供了最常见操作的包装,包括将整个流序列化为字符串。
 */
interface StreamInterface
{
    /**
     * 从头到尾将流中的所有数据读取到字符串。
     *
     * 这个方法 **必须** 在开始读数据前定位到流的开头,并读取出所有的数据。
     *
     * 警告:这可能会尝试将大量数据加载到内存中。
     *
     * 这个方法 **不得** 抛出异常以符合 PHP 的字符串转换操作。
     *
     * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
     * @return string
     */
    public function __toString();

    /**
     * 关闭流和任何底层资源。
     *
     * @return void
     */
    public function close();

    /**
     * 从流中分离任何底层资源。
     *
     * 分离之后,流处于不可用状态。
     *
     * @return resource|null 如果存在的话,返回底层 PHP 流。
     */
    public function detach();

    /**
     * 如果可知,获取流的数据大小。
     *
     * @return int|null 如果可知,返回以字节为单位的大小,如果未知返回 `null`。
     */
    public function getSize();

    /**
     * 返回当前读/写的指针位置。
     *
     * @return int 指针位置。
     * @throws \RuntimeException 产生错误时抛出。
     */
    public function tell();

    /**
     * 返回是否位于流的末尾。
     *
     * @return bool
     */
    public function eof();

    /**
     * 返回流是否可随机读取。
     *
     * @return bool
     */
    public function isSeekable();

    /**
     * 定位流中的指定位置。
     *
     * @see http://www.php.net/manual/en/function.fseek.php
     * @param int $offset 要定位的流的偏移量。
     * @param int $whence 指定如何根据偏移量计算光标位置。有效值与 PHP 内置函数 `fseek()` 相同。
     *     SEEK_SET:设定位置等于 $offset 字节。默认。
     *     SEEK_CUR:设定位置为当前位置加上 $offset。
     *     SEEK_END:设定位置为文件末尾加上 $offset (要移动到文件尾之前的位置,offset 必须是一个负值)。
     * @throws \RuntimeException 失败时抛出。
     */
    public function seek($offset, $whence = SEEK_SET);

    /**
     * 定位流的起始位置。
     *
     * 如果流不可以随机访问,此方法将引发异常;否则将执行 seek(0)。
     *
     * @see seek()
     * @see http://www.php.net/manual/en/function.fseek.php
     * @throws \RuntimeException 失败时抛出。
     */
    public function rewind();

    /**
     * 返回流是否可写。
     *
     * @return bool
     */
    public function isWritable();

    /**
     * 向流中写数据。
     *
     * @param string $string 要写入流的数据。
     * @return int 返回写入流的字节数。
     * @throws \RuntimeException 失败时抛出。
     */
    public function write($string);

    /**
     * 返回流是否可读。
     *
     * @return bool
     */
    public function isReadable();

    /**
     * 从流中读取数据。
     *
     * @param int $length 从流中读取最多 $length 字节的数据并返回。如果数据不足,则可能返回少于
     *     $length 字节的数据。
     * @return string 返回从流中读取的数据,如果没有可用的数据则返回空字符串。
     * @throws \RuntimeException 失败时抛出。
     */
    public function read($length);

    /**
     * 返回字符串中的剩余内容。
     *
     * @return string
     * @throws \RuntimeException 如果无法读取则抛出异常。
     * @throws \RuntimeException 如果在读取时发生错误则抛出异常。
     */
    public function getContents();

    /**
     * 获取流中的元数据作为关联数组,或者检索指定的键。
     *
     * 返回的键与从 PHP 的 stream_get_meta_data() 函数返回的键相同。
     *
     * @see http://php.net/manual/en/function.stream-get-meta-data.php
     * @param string $key 要检索的特定元数据。
     * @return array|mixed|null 如果没有键,则返回关联数组。如果提供了键并且找到值,
     *     则返回特定键值;如果未找到键,则返回 null。
     */
    public function getMetadata($key = null);
}

3.5 Psr\Http\Message\UriInterface

<?php
namespace Psr\Http\Message;

/**
 * URI 数据对象。
 *
 * 此接口按照 RFC 3986 来构建 HTTP URI,提供了一些通用的操作,你可以自由的对此接口
 * 进行扩展。你可以使用此 URI 接口来做 HTTP 相关的操作,也可以使用此接口做任何 URI 
 * 相关的操作。
 *
 * 此接口的实例化对象被视为无法修改的,所有能修改状态的方法,都 **必须** 有一套机制,在内部保
 * 持好原有的内容,然后把修改状态后的,新的实例返回。
 *
 * 通常,HOST 信息也将出现在请求消息中。对于服务器端的请求,通常可以在服务器参数中发现此信息。
 * 
 * @see [URI 通用标准规范](http://tools.ietf.org/html/rfc3986)
 */
interface UriInterface
{
    /**
     * 从 URI 中取出 scheme。
     *
     * 如果不存在 Scheme,此方法 **必须** 返回空字符串。
     *
     * 根据 RFC 3986 规范 3.1 章节,返回的数据 **必须** 是小写字母。
     *
     * 最后部分的「:」字串不属于 Scheme,**不得** 作为返回数据的一部分。
     *
     * @see https://tools.ietf.org/html/rfc3986#section-3.1
     * @return string URI Ccheme 的值。
     */
    public function getScheme();

    /**
     * 返回 URI 认证信息。
     *
     * 如果没有 URI 认证信息的话,**必须** 返回一个空字符串。
     *
     * URI 的认证信息语法是:
     *
     * <pre>
     * [user-info@]host[:port]
     * </pre>
     *
     * 如果端口部分没有设置,或者端口不是标准端口,**不应该** 包含在返回值内。
     *
     * @see https://tools.ietf.org/html/rfc3986#section-3.2
     * @return string URI 认证信息,格式为:「[user-info@]host[:port]」。
     */
    public function getAuthority();

    /**
     * 从 URI 中获取用户信息。
     *
     * 如果不存在用户信息,此方法 **必须** 返回一个空字符串。
     * 
     * 如果 URI 中存在用户,则返回该值;此外,如果密码也存在,它将附加到用户值,用冒号(「:」)分隔。
     *
     * 用户信息后面跟着的 "@" 字符,不是用户信息里面的一部分,**不得** 在返回值里出现。
     *
     * @return string URI 的用户信息,格式:"username[:password]" 
     */
    public function getUserInfo();

    /**
     * 从 URI 中获取 HOST 信息。
     *
     * 如果 URI 中没有此值,**必须** 返回空字符串。
     *
     * 根据 RFC 3986 规范 3.2.2 章节,返回的数据 **必须** 是小写字母。
     *
     * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
     * @return string URI 中的 HOST 信息。
     */
    public function getHost();

    /**
     * 从 URI 中获取端口信息。
     *
     * 如果端口信息是与当前 Scheme 的标准端口不匹配的话,就使用整数值的格式返回,如果是一
     * 样的话,**应该** 返回 `null` 值。
     * 
     * 如果不存在端口和 Scheme 信息,**必须** 返回 `null` 值。
     * 
     * 如果不存在端口数据,但是存在 Scheme 的话,**可能** 返回 Scheme 对应的
     * 标准端口,但是 **应该** 返回 `null`。
     * 
     * @return null|int URI 中的端口信息。
     */
    public function getPort();

    /**
     * 从 URI 中获取路径信息。
     *
     * 路径可以是空的,或者是绝对的(以斜线「/」开头),或者相对路径(不以斜线开头)。
     * 实现 **必须** 支持所有三种语法。
     *
     * 根据 RFC 7230 第 2.7.3 节,通常空路径「」和绝对路径「/」被认为是相同的。
     * 但是这个方法 **不得** 自动进行这种规范化,因为在具有修剪的基本路径的上下文中,
     * 例如前端控制器中,这种差异将变得显著。用户的任务就是可以将「」和「/」都处理好。
     *
     * 返回的值 **必须** 是百分号编码,但 **不得** 对任何字符进行双重编码。
     * 要确定要编码的字符,请参阅 RFC 3986 第 2 节和第 3.3 节。
     *
     * 例如,如果值包含斜线(「/」)而不是路径段之间的分隔符,则该值必须以编码形式(例如「%2F」)
     * 传递给实例。
     *
     * @see https://tools.ietf.org/html/rfc3986#section-2
     * @see https://tools.ietf.org/html/rfc3986#section-3.3
     * @return string URI 路径信息。
     */
    public function getPath();

    /**
     * 获取 URI 中的查询字符串。
     *
     * 如果不存在查询字符串,则此方法必须返回空字符串。
     *
     * 前导的「?」字符不是查询字符串的一部分,**不得** 添加在返回值中。
     *
     * 返回的值 **必须** 是百分号编码,但 **不得** 对任何字符进行双重编码。
     * 要确定要编码的字符,请参阅 RFC 3986 第 2 节和第 3.4 节。
     *
     * 例如,如果查询字符串的键值对中的值包含不做为值之间分隔符的(「&」),则该值必须
     * 以编码形式传递(例如「%26」)到实例。
     *
     * @see https://tools.ietf.org/html/rfc3986#section-2
     * @see https://tools.ietf.org/html/rfc3986#section-3.4
     * @return string URI 中的查询字符串
     */
    public function getQuery();

    /**
     * 获取 URI 中的片段(Fragment)信息。
     *
     * 如果没有片段信息,此方法 **必须** 返回空字符串。
     *
     * 前导的「#」字符不是片段的一部分,**不得** 添加在返回值中。
     *
     * 返回的值 **必须** 是百分号编码,但 **不得** 对任何字符进行双重编码。
     * 要确定要编码的字符,请参阅 RFC 3986 第 2 节和第 3.5 节。
     *
     * @see https://tools.ietf.org/html/rfc3986#section-2
     * @see https://tools.ietf.org/html/rfc3986#section-3.5
     * @return string URI 中的片段信息。
     */
    public function getFragment();

    /**
     * 返回具有指定 Scheme 的实例。
     *
     * 此方法 **必须** 保留当前实例的状态,并返回包含指定 Scheme 的实例。
     *
     * 实现 **必须** 支持大小写不敏感的「http」和「https」的 Scheme,并且在
     * 需要的时候 **可能** 支持其他的 Scheme。
     *
     * 空的 Scheme 相当于删除 Scheme。
     *
     * @param string $scheme 给新实例使用的 Scheme。
     * @return self 具有指定 Scheme 的新实例。
     * @throws \InvalidArgumentException 使用无效的 Scheme 时抛出。
     * @throws \InvalidArgumentException 使用不支持的 Scheme 时抛出。
     */
    public function withScheme($scheme);

    /**
     * 返回具有指定用户信息的实例。
     *
     * 此方法 **必须** 保留当前实例的状态,并返回包含指定用户信息的实例。
     *
     * 密码是可选的,但用户信息 **必须** 包括用户;用户信息的空字符串相当于删除用户信息。
     * 
     * @param string $user 用于认证的用户名。
     * @param null|string $password 密码。
     * @return self 具有指定用户信息的新实例。
     */
    public function withUserInfo($user, $password = null);

    /**
     * 返回具有指定 HOST 信息的实例。
     *
     * 此方法 **必须** 保留当前实例的状态,并返回包含指定 HOST 信息的实例。
     *
     * 空的 HOST 信息等同于删除 HOST 信息。
     *
     * @param string $host 用于新实例的 HOST 信息。
     * @return self 具有指定 HOST 信息的实例。
     * @throws \InvalidArgumentException 使用无效的 HOST 信息时抛出。
     */
    public function withHost($host);

    /**
     * 返回具有指定端口的实例。
     *
     * 此方法 **必须** 保留当前实例的状态,并返回包含指定端口的实例。
     *
     * 实现 **必须** 为已建立的 TCP 和 UDP 端口范围之外的端口引发异常。
     *
     * 为端口提供的空值等同于删除端口信息。
     *
     * @param null|int $port 用于新实例的端口;`null` 值将删除端口信息。
     * @return self 具有指定端口的实例。
     * @throws \InvalidArgumentException 使用无效端口时抛出异常。
     */
    public function withPort($port);

    /**
     * 返回具有指定路径的实例。
     *
     * 此方法 **必须** 保留当前实例的状态,并返回包含指定路径的实例。
     *
     * 路径可以是空的、绝对的(以斜线开头)或者相对路径(不以斜线开头),实现必须支持这三种语法。
     *
     * 如果 HTTP 路径旨在与 HOST 相对而不是路径相对,,那么它必须以斜线开头。
     * 假设 HTTP 路径不以斜线开头,对应该程序或开发人员来说,相对于一些已知的路径。
     *
     * 用户可以提供编码和解码的路径字符,要确保实现了 `getPath()` 中描述的正确编码。
     *
     * @param string $path 用于新实例的路径。
     * @return self 具有指定路径的实例。
     * @throws \InvalidArgumentException 使用无效的路径时抛出。
     */
    public function withPath($path);

    /**
     * 返回具有指定查询字符串的实例。
     *
     * 此方法 **必须** 保留当前实例的状态,并返回包含查询字符串的实例。
     *
     * 用户可以提供编码和解码的查询字符串,要确保实现了 `getQuery()` 中描述的正确编码。
     *
     * 空查询字符串值等同于删除查询字符串。
     *
     * @param string $query 用于新实例的查询字符串。
     * @return self 具有指定查询字符串的实例。
     * @throws \InvalidArgumentException 使用无效的查询字符串时抛出。
     */
    public function withQuery($query);

    /**
     * 返回具有指定 URI 片段(Fragment)的实例。
     *
     * 此方法 **必须** 保留当前实例的状态,并返回包含片段的实例。
     *
     * 用户可以提供编码和解码的片段,要确保实现了 `getFragment()` 中描述的正确编码。
     *
     * 空片段值等同于删除片段。
     *
     * @param string $fragment 用于新实例的片段。
     * @return self 具有指定 URI 片段的实例。
     */
    public function withFragment($fragment);

    /**
     * 返回字符串表示形式的 URI。
     *
     * 根据 RFC 3986 第 4.1 节,结果字符串是完整的 URI 还是相对引用,取决于 URI 有哪些组件。
     * 该方法使用适当的分隔符连接 URI 的各个组件:
     *
     * - 如果存在 Scheme 则 **必须** 以「:」为后缀。
     * - 如果存在认证信息,则必须以「//」作为前缀。
     * - 路径可以在没有分隔符的情况下连接。但是有两种情况需要调整路径以使 URI 引用有效,因为 PHP
     *   不允许在 `__toString()` 中引发异常:
     *     - 如果路径是相对的并且有认证信息,则路径 **必须** 以「/」为前缀。
     *     - 如果路径以多个「/」开头并且没有认证信息,则起始斜线 **必须** 为一个。
     * - 如果存在查询字符串,则 **必须** 以「?」作为前缀。
     * - 如果存在片段(Fragment),则 **必须** 以「#」作为前缀。
     *
     * @see http://tools.ietf.org/html/rfc3986#section-4.1
     * @return string
     */
    public function __toString();
}

3.6 Psr\Http\Message\UploadedFileInterface

<?php
namespace Psr\Http\Message;

/**
 * 通过 HTTP 请求上传的一个文件内容。
 *
 * 此接口的实例是被视为无法修改的,所有能修改状态的方法,都 **必须** 有一套机制,在内部保
 * 持好原有的内容,然后把修改状态后的,新的实例返回。
 */
interface UploadedFileInterface
{
    /**
     * 获取上传文件的数据流。
     *
     * 此方法必须返回一个 `StreamInterface` 实例,此方法的目的在于允许 PHP 对获取到的数
     * 据流直接操作,如 stream_copy_to_stream() 。
     *
     * 如果在调用此方法之前调用了 `moveTo()` 方法,此方法 **必须** 抛出异常。
     *
     * @return StreamInterface 上传文件的数据流
     * @throws \RuntimeException 没有数据流的情形下。
     * @throws \RuntimeException 无法创建数据流。
     */
    public function getStream();

    /**
     * 把上传的文件移动到新目录。
     *
     * 此方法保证能同时在 `SAPI` 和 `non-SAPI` 环境下使用。实现类库 **必须** 判断
     * 当前处在什么环境下,并且使用合适的方法来处理,如 move_uploaded_file(), rename()
     * 或者数据流操作。
     *
     * $targetPath 可以是相对路径,也可以是绝对路径,使用 rename() 解析起来应该是一样的。
     *
     * 当这一次完成后,原来的文件 **必须** 会被移除。
     * 
     * 如果此方法被调用多次,一次以后的其他调用,都要抛出异常。
     *
     * 如果在 SAPI 环境下的话,$_FILES 内有值,当使用  moveTo(), is_uploaded_file()
     * 和 move_uploaded_file() 方法来移动文件时 **应该** 确保权限和上传状态的准确性。
     * 
     * 如果你希望操作数据流的话,请使用 `getStream()` 方法,因为在 SAPI 场景下,无法
     * 保证书写入数据流目标。
     * 
     * @see http://php.net/is_uploaded_file
     * @see http://php.net/move_uploaded_file
     * @param string $targetPath 目标文件路径。
     * @throws \InvalidArgumentException 参数有问题时抛出异常。
     * @throws \RuntimeException 发生任何错误,都抛出此异常。
     * @throws \RuntimeException 多次运行,也抛出此异常。
     */
    public function moveTo($targetPath);

    /**
     * 获取文件大小。
     *
     * 实现类库 **应该** 优先使用 $_FILES 里的 `size` 数值。
     * 
     * @return int|null 以 bytes 为单位,或者 null 未知的情况下。
     */
    public function getSize();

    /**
     * 获取上传文件时出现的错误。
     *
     * 返回值 **必须** 是 PHP 的 UPLOAD_ERR_XXX 常量。
     *
     * 如果文件上传成功,此方法 **必须** 返回 UPLOAD_ERR_OK。
     *
     * 实现类库 **必须** 返回 $_FILES 数组中的 `error` 值。
     * 
     * @see http://php.net/manual/en/features.file-upload.errors.php
     * @return int PHP 的 UPLOAD_ERR_XXX 常量。
     */
    public function getError();

    /**
     * 获取客户端上传的文件的名称。
     * 
     * 永远不要信任此方法返回的数据,客户端有可能发送了一个恶意的文件名来攻击你的程序。
     * 
     * 实现类库 **应该** 返回存储在 $_FILES 数组中 `name` 的值。
     *
     * @return string|null 用户上传的名字,或者 null 如果没有此值。
     */
    public function getClientFilename();

    /**
     * 客户端提交的文件类型。
     * 
     * 永远不要信任此方法返回的数据,客户端有可能发送了一个恶意的文件类型名称来攻击你的程序。
     *
     * 实现类库 **应该** 返回存储在 $_FILES 数组中 `type` 的值。
     *
     * @return string|null 用户上传的类型,或者 null 如果没有此值。
     */
    public function getClientMediaType();
}

PSR-11 容器接口

本文描述了依赖注入容器的通用接口。

设定 ContainerInterface 的目的是为了标准化框架或类库如何使用容器来获取对象和参数。(本文其它部分称之为 实体 )。

为了避免歧义,文档大量使用了「能愿动词」,对应的解释如下:

必须 (MUST):绝对,严格遵循,请照做,无条件遵守;
一定不可 (MUST NOT):禁令,严令禁止;
应该 (SHOULD) :强烈建议这样做,但是不强求;
不该 (SHOULD NOT):强烈不建议这样做,但是不强求;
可以 (MAY) 和 可选 (OPTIONAL) :选择性高一点,在这个文档内,此词语使用较少;

参见:RFC 2119

本文中关键字 implementor 被看作某些在依赖注入相关的框架或类库中实现了 ContainerInterface 接口。 使用依赖注入容器(DIC)的用户被看作 user

1、规范

1.1 基础知识

1.1.1 实体标识符

实体标识符是一个任何合法的 PHP 字符串,它至少包含 1 个字符的,它用来唯一标识容器里的一个对象。 实体标识符只是一个不透明的字符串,所以调用者不应该通过语义去猜测它具有的结构。

1.1.2 容器的方法

  • Psr\Container\ContainerInterface 接口提供了两个方法: gethas

  • get 方法有一个必传的参数:一个字符串格式的实体标识符。 get 方法可以返回任何类型的值, 或者在容器没有标识符对应值的时候抛出一个 NotFoundExceptionInterface 接口实现类的异常。 连续两次使用相同参数调用 get 方法得到的值应该是相同的,然而,这取决于 implementor 实现类的设计和 user 用户配置, 可能也会返回不同的值。所以 user 用户不应该依赖在两次连续调用时可以获得相同的值。

  • has 方法需要一个唯一参数:一个字符串格式的实体标识符。如果容器内有标识符对应的内容时 has 方法返回 true 值; 否则 has 方法返回 false 。如果调用 has($id) 返回了 false , 那么相同 $id 调用 get($id) 方法一定是抛出 NotFoundExceptionInterface 接口的异常。

1.2 异常

容器抛出的异常都需要实现 Psr\Container\ContainerExceptionInterface 接口。

通过 get 方法获取一个容器中不存在实体标识符时必须抛出 Psr\Container\NotFoundExceptionInterface 接口的异常实现类。

1.3 推荐用法

用户 不应该 将容器作为参数传入对象然后在对象中通过容器获得对象的依赖。这样是把容器当作 服务定位器 使用,而服务定位器是一个不受欢迎的模式。

相关的详情信息,请查看文档的第 4 部分。

2、包

psr/container 包中提供了上面提到的接口和相关异常类。

实现 PSR 容器接口的包应该申明为 psr/container-implementation 1.0.0 包。

需要使用容器的项目只需要引入上面实现的包 psr/container-implementation 1.0.0 即可。

3、接口

3.1. Psr\Container\ContainerInterface

<?php
namespace Psr\Container;

/**
 * 容器的接口类,提供了获取容器中对象的方法。
 */
interface ContainerInterface
{
    /**
     * 在容器中查找并返回实体标识符对应的对象。
     *
     * @param string $id 查找的实体标识符字符串。
     *
     * @throws NotFoundExceptionInterface  容器中没有实体标识符对应对象时抛出的异常。
     * @throws ContainerExceptionInterface 查找对象过程中发生了其他错误时抛出的异常。
     *
     * @return mixed 查找到的对象。
     */
    public function get($id);

    /**
     * 如果容器内有标识符对应的内容时,返回 true 。
     * 否则,返回 false。
     *
     * 调用 `has($id)` 方法返回 true,并不意味调用  `get($id)` 不会抛出异常。
     * 而只意味着 `get($id)` 方法不会抛出 `NotFoundExceptionInterface` 实现类的异常。
     *
     * @param string $id 查找的实体标识符字符串。
     *
     * @return bool
     */
    public function has($id);
}

3.2. Psr\Container\ContainerExceptionInterface

<?php
namespace Psr\Container;

/**
 * 容器中的基础异常类。
 */
interface ContainerExceptionInterface
{
}

3.3. Psr\Container\NotFoundExceptionInterface

<?php
namespace Psr\Container;

/**
 * 容器中没有查找到对应对象时的异常
 */
interface NotFoundExceptionInterface extends ContainerExceptionInterface
{
}

PSR-11 容器接口 - 说明文档

容器元文档

1、介绍

文档介绍了容器 PSR 出现的过程和争论。目的是解释每个决定背后的原因。

2、为什么要 PSR

下面列举了 10 多个依赖注入容器,它们使用各种各样的方法来保存对象。

有一些基于回调函数( Pimple,Laravel,...)
另一些基于不同格式( PHP 数组,YAML 文件,XML 文件)的配置( Symfony,ZF,...)
有一些可以利用工厂模式...
有一些使用 PHP API 来创建对象 ( PHP-DI,ZF,Symfony,Mouf... )
有一些可以进行自动装载对象( Laravel,PHP-DI,... )
另一些可以基于注解来装载对象( PHP-DI,JMS Bundle... )
有一些提供图形用户界面( Mouf... )
有一些可以编译配置文件到 PHP 类中( Symfony,ZF... )
有一些可以使用对象别名...
有一些可以使用代理来提供依赖的延迟加载...

所以当你了解了整体情况后,你会发现有很多不同的方法来解决 DI 问题,因此也有很多不同的容器实现。 然而,所有的 DI 容器都是为了解决一个相同的问题:给应用提供一种方法来查找、获取配置的对象(通常是应用需要的服务)。

通过标准化从容器中获取对象的方法,可以让使用 PSR 容器规范的框架和库可以选择使用任何与之兼容的容器类。 这样就能让终端用户根据自己的喜好来选择他们自己的容器。

3、容器规范的范围

3.1. 目的

容器 PSR 规范的目的是通过标准化框架和库通过容器获取对象的方法和参数。

区分容器下面的两个用法是很重要的:

  • 配置对象实例
  • 获取对象实例

通常情况下,相同地方不会同时需要这两种不同方法。通常框架使用容器来获取对象构建应用,而终端用户倾向于使用它来配置对象。

这是为什么这个接口只关注从容器获取对象的原因。

3.2. 不包括的目的

对象在容器中怎么保存和怎么配置不是 PSR 规范的范围。这也是不同容器可有的独特之处。 一些容器根本没有配置(它依赖自动装载),一些依赖 PHP 回调来定义,另一些依赖配置文件… PSR 规范只关注怎么从容器中获取对象。

此外,对象的命名约定也不在 PSR 规范的范围内。事实上,你可以发现有下面两种命名策略:

  • 实体标识符为类名或者接口名(大多数可以自动装载的框架这么用)
  • 实体标识符为一个普通的名称(更接近于变量名),大多数依赖配置的框架这样使用。

两种方式都有各自的优点和缺点。PSR 规范的目的不是从中选择一个作为规范。 相反,用户可以使用别名的方式在两个不同命名策略的容器间做兼容。

4、推荐用法:容器 PSR 和服务定位器

PSR 指出:

「用户不应该将容器作为参数传入对象然后在对象中通过容器获得对象的依赖。这样是把容器当作服务定位器来使用,而服务定位器是不受欢迎的模式」
// 这是不推荐的,容器被当作服务定位器来使用了
class BadExample
{
    public function __construct(ContainerInterface $container)
    {
        $this->db = $container->get('db');
    }
}

// 可以考虑使用直接注入的方式,替代上面的方式
class GoodExample
{
    public function __construct($db)
    {
        $this->db = $db;
    }
}
// 然后,你可以使用容器来将 $db 对象注入到 $goodExample 类中。

不应该在 BadExample 类注入容器的原因:

  • 这样减少了代码的兼容性:通过注入容器,你不得不使用兼容 PSR 规范的容器。而通过直接注入方式,你的代码可以使用任何容器。
  • 这样将强制使开发者使用「db」作为数据库的实体标识符。这个命名可能与其他包(使用 「db」 来获取其他服务)产生冲突。
  • 这样将使测试变得困难。
  • 这样在代码中不能明显看出 BadExample 类依赖 「db」服务。依赖关系被隐藏了。

通常, ContainerInterface 接口是被其他包使用。而作为使用框架的 PHP 开发者,不太可能需要直接使用 ContainerInterface 的接口和类型提示。

判断你的代码是否合理的使用了容器,归结于知道在容器中查找的对象是否为当前对象的依赖。下面是几个实例:

class RouterExample
{
    // ...

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function getRoute($request)
    {
        $controllerName = $this->getContainerEntry($request->getUrl());
        // 这是正确的,路由通过容器查找对应的控制器对象,
        // 而路由不依赖控制器
        $controller = $this->container->get($controllerName);
        // ...
    }
}

在这个示例中,路由将 URL 转换为控制器类名,然后从容器中获得控制器对象。但路由并不真正的依赖控制器。 大致的原则是,如果对象需要计算并从一系列的对象列表中得到对应的对象,你的使用通常是合理的。

有一个例外,作为只是单纯创建和返回对象实例的工厂类是可以使用服务定位器的。 工厂类必须实现一个接口,以至于它可以被实现相同接口的其它工厂类替换。

// 这是合理的:一个创建对象的工厂接口和它的实现
interface FactoryInterface
{
    public function newInstance();
}

class ExampleFactory implements FactoryInterface
{
    protected $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function newInstance()
    {
        return new Example($this->container->get('db'));
    }
}

5、历史

在提交容器 PSR 到 PHP-FIG 组织之前,ContainerInterface 接口是在名为 container-interop 的项目中首先提出的。 这个项目的目的是为实施 ContainerInterface 接口提供实验平台,并为容器 PSR 铺路。

在接下来的文档中,你会看到频繁的引用 container-interop 。

6、接口名称

接口名称与 container-interop 中讨论的一致(只是为了符合其他 PSRs 而改变了命名空间)。 接口名称是在 container-interop 中彻底讨论,并投票决定的 。

下面是他们投票选项的结果:

ContainerInterface: +8
ProviderInterface: +2
LocatorInterface: 0
ReadableContainerInterface: -5
ServiceLocatorInterface: -6
ObjectFactory: -6
ObjectStore: -8
ConsumerInterface: -9

7、接口方法

接口需要包含那些方法是通过对现有的容器进行统计分析后得到的 。

统计分析的概要如下:

所用的容器都提供了通过 id 获取对应对象的方法
大多数使用的方法名称是 get()
所有的容器中,get() 方法都有一个必须的字符串参数
一些容器的 get() 方法有一个其他可选的参数,但是不同容器的可选参数作用不一样
大多数容器都提供了一个测试是否可以通过 id 获取到对应对象的方法
大多数使用的方法名称为 has()
对于所有提供了 has() 方法的容器,它们都有一个字符串参数
大多数容器在 get() 方法没有查到对象时抛出异常,而不是返回 null
大多数容器都没有实现 ArrayAccess 接口

容器中是否需要提供方法来定义对象,在 container-interop 项目开始时已经被讨论过了 。 讨论的结果是接口不需要提供这个方法,因为它不在容器接口的目的中(查看「目的」部分)。

结果,ContainerInterface 接口提供两个方法:

get() 方法,有一个必须的字符串参数,可返回任何对象。如果没有找到参数对应的对象时抛出异常。
has() 方法,有一个必须的字符串参数,返回布尔值。

7.1. get() 方法的参数个数

ContainerInterface 接口的 get() 方法只定义了一个必须的参数,这与当前有其他可选参数的容器不兼容。 但 PHP 允许实现类拥有更多的参数,只要参数是可选的,因为实现类这样是符合接口的要求的。

PSR 容器与 container-interop 规范规定的不同, container-interop 规范 指出:

尽管 ContainerInterface 接口的 get() 方法只定义了一个必须的参数,但它的实现类可以接受其他的可选参数。

但这个语句在 PSR-11 中被删除了,因为:

接受更多可选参数违背 PHP 面向对象原则,这和 PSR-11 没有直接关系
我们不鼓励接口的实现类添加额外的可选参数,因为我们推荐面向接口编程而不是面向实现编程

然而,一些已经实现的有其他可选参数的容器;这在技术上是合法的。这些容器也与 PSR-11 兼容 。

7.2. 参数 $id 的类型

在 container-interop 项目中已经讨论了 get() 和 has() 方法中 $id 参数的类型。

尽管所有分析的容器中 $id 参数都是 string 类型,但是建议允许它可以是任何类型(比如对象),这样将允许容器提供更多高级的查询 API。

例如使用容器来作为对象构造器,$id 参数是对象就可以告诉容器怎么去创建一个对象实例。

讨论的结果 是这超出了 $id 是用来从容器获取对象的范围, $id 是不知道对象是怎么创建的。对象参数更适合工厂类。

7.3. 抛出异常

PSR 提供了 2 个用来被容器异常实现的接口。

7.3.1 异常基类

Psr\Container\ContainerExceptionInterface 接口是异常基类。从容器中抛出的自定义异常都应该实现这个接口。

任何属于容器部分的异常都应该实现 ContainerExceptionInterface 接口,下面是几个例子:

  • 如果容器依赖配置文件,而配置文件又存在缺陷时,容器可能会抛出一个实现 ContainerExceptionInterface 接口的 InvalidFileException 异常。
  • 如果依赖关系中检测到存在循环依赖,容器可能会抛出一个实现 ContainerExceptionInterface 接口的 CyclicDependencyException 异常。

然而,如果抛出异常的代码在容器范围外(例如,初始化对象时抛出异常), 这时容器抛出的自定义异常不要求实现 ContainerExceptionInterface 基类接口。

异常基类接口的作用被质疑:它不是通用的会被捕获的异常 。

然而,大多数 PHP-FIG 成员认为异常基类是一个最佳实践。原有的 PSR 容器和几个成员的项目都已经实现了异常基类。 因此异常基类被保留下来了。

7.3.2 未找到异常 Not found exception

参数 id 对应的对象在容器中不存在时, get 方法抛出的异常必须实现 Psr\Container\NotFoundExceptionInterface 接口。

对于给定的标识符:

  • 如果 has 方法返回 false , get 方法抛出的异常一定要实现 Psr\Container\NotFoundExceptionInterface 接口。
  • 如果 has 方法返回 true,这并不意味 get 会成功且不会抛出异常。 如果对象依赖的对象不存在时也会抛出 Psr\Container\NotFoundExceptionInterface 接口的异常。

因此,如果用户捕获到了实现 Psr\Container\NotFoundExceptionInterface 接口的异常,可能意味着两种情况 :

请求的对象不存在(错误的请求)
或者请求对象的依赖不存在(比如容器的配置错误)

用户可以通过 has 方法轻松地区分上面两种情况。

伪代码如下:

if (!$container->has($id)) {
    // 请求的对象不存在
    return;
}
try {
    $entry = $container->get($id);
} catch (NotFoundExceptionInterface $e) {
    // 因为请求的对象存在,所以 NotFoundExceptionInterface 的异常表示这是容器配置错误或者请求对象的依赖不存在。
}
已有的容器实现

在写这篇文字的时候,下列项目已经实现或者使用了 container-interop 版本的接口。

容器的实现

Acclimate
Aura.DI
dcp-di
League Container
Mouf
Njasm Container
PHP-DI
PimpleInterop
XStatic
Zend ServiceManager

中间件

Alias-Container
Prefixer-Container

容器使用者

Behat
interop.silex.di
mindplay/middleman
PHP-DI Invoker
Prophiler
Silly
Slim
Splash
Zend Expressive

这个列表不包含所有的容器实现和使用者,这里仅仅是一些对 PSR 有着巨大兴趣的项目例子。

人员

9.1 编者

Matthieu Napoli
David Négrier

9.2 发起人

Matthew Weier O'Phinney (Coordinator)
Korvin Szanto

9.3 贡献者

这里列出的是所有参与过讨论或投票的人员(在 container-interop 项目或者是项目迁移到 PSR-11 的期间),人名按字母顺序排列:

Alexandru Pătrănescu
Amy Stephen
Ben Peachey
David Négrier
Don Gilbert
Jason Judge
Jeremy Lindblom
Larry Garfield
Marco Pivetta
Matthieu Napoli
Nelson J Morais
Paul M. Jones
Phil Sturgeon
Stephan Hochdörfer
Taylor Otwell

10、相关链接

容器 PSR 和服务定位器的讨论
Container-interop 项目的  ContainerI...
所有的 issues
接口名称和 container-interop 项目范围的讨...
接口名称的投票
已有容器方法名称的统计分析
方法名称和参数的讨论
异常基类作用的讨论
NotFoundExceptionInterface...
在 container-interop  项目和 在 PHP-FIG mailing list 对 get 方法可选参数的讨论

PSR-12 编码规范扩充

编码风格扩充指南

文章中的关键词 MUST , MUST NOT , REQUIRED , SHALL , SHALL NOT , SHOULD , SHOULD NOT , RECOMMENDED , MAY ,和 OPTIONAL 都在 RFC 2119 中进行来解释。

摘要

此规范起到继承,扩展和替换 [PSR-2][] 的作用, 同时编码风格遵守 [PSR-1][] 这个基础编码标准。

和 [PSR-2][] 一样, 此规范的目的是减少不同人在阅读代码时认知冲突。 它通过列举一套如何格式化 PHP 代码的公共的规则和期望来实现这个目标。 PSR 力图提供一套方法,编码风格工具可以利用,项目可以遵守,开发人员可以方便的在不同的项目中使用。 当各个的开发人员在进行多项目合作的时候,它可以帮助在这些项目中提供一套通用的指导。 所以,本指南的价值不是规则本身,而是这些规则的共享。

[PSR-2][] 在 2012 年被接受,随后 PHP 经历了很多变化,影响了编码风格。 同时 [PSR-2] 是 PHP 编码时候的基础功能,被广泛的采用。 因此,PSR 力图通过一种更加现代的方式说明 PSR-2 的内容和新功能,并对 PSR-2 进行更正。

以前的语言版本

在整个文档中,任何说明都可以被忽略,如果它们不存在于你项目所支持的 PHP 版本中。

例如

此示例包含以下一些规则作为快速概述:

<?php

declare(strict_types=1);

namespace Vendor\Package;

use Vendor\Package\{ClassA as A, ClassB, ClassC as C};
use Vendor\Package\SomeNamespace\ClassD as D;

use function Vendor\Package\{functionA, functionB, functionC};

use const Vendor\Package\{ConstantA, ConstantB, ConstantC};

class Foo extends Bar implements FooInterface
{
    public function sampleFunction(int $a, int $b = null): array
    {
        if ($a === $b) {
            bar();
        } elseif ($a > $b) {
            $foo->bar($arg1);
        } else {
            BazClass::bar($arg2, $arg3);
        }
    }

    final public static function bar()
    {
        // 方法内容
    }
}

2、总则

2.1 基本编码标准

代码必须遵循 [PSR-1] 中列出的所有规则。

PSR-1 中的术语 ‘StudlyCaps’ 必须解释为 PascalCase (帕斯卡命名法:大驼峰式命名法), 其中每个单词的第一个字母大写,包括第一个字母。

2.2 文件

所有 PHP 文件只能使用 Unix LF (换行符) 结尾。

所有的 PHP 文件都必须以非空行结尾,以一个 LF 结尾。

在仅包含 PHP 代码的文件中,必须省略结尾的 ?> 标记。

2.3 代码行

行长度不得有硬限制。

行长度的软限制必须为 120 个字符。

行的长度不应超过 80 个字符;超过该长度的行应拆分为多个后续行,每个行的长度不应超过 80 个字符。

行尾不能有尾随空格。

可以添加空行以提高可读性并指示相关的代码块,除非明确禁止。

每行不能有多个语句。

2.4 缩进

代码必须为每个缩进级别使用 4 个空格的缩进,并且不能使用缩进标签。

2.5 关键词和类型

PHP 的所有关键字和类型 都必须使用小写。

PHP 未来版本中新加的所有关键字和类型也都必须使用小写。

类型关键字必须使用缩写。使用 bool 而不是 boolean,使用 int 而不是 integer 等等。

3、声明、命名空间以及导入

一个 PHP 文件的头部可能会包含多个块。如果包含多个块,则每个块都必须用空白行和其他块分隔,并且块内不能包含空白行。 所有的块都必须按照下面的顺序排列,如果不存在该块则忽略。

  • PHP 文件开始标签: <?php。
  • 文件级文档块。
  • 一个或多个声明语句。
  • 命名空间声明语句。
  • 一个或多个基于类的 use 声明语句。
  • 一个或多个基于方法的 use 声明语句。
  • 一个或多个基于常量的 use 声明语句。
  • 其余代码。

当文件包含 HTML 和 PHP 的混合代码时,可以使用上面列出的任何部分。如果是这种情况的话, 即是代码的其他部分包含有 PHP 结束符,然后再包含 HTML 和 PHP 代码,声明、命名空间和导入语句块也必须放在文件的顶部。

什么时候开始 <?php 标签位于文件的第一行,它必须位于自己的行,没有其他语句, 除非它是一个包含 PHP 之外的标记的文件打开和关闭标记。

import 语句不能以前导反斜杠开头,因为它们必须始终完全合格。

以下示例演示了所有块的完整列表:

<?php

/**
 * This file contains an example of coding styles.
 */

declare(strict_types=1);

namespace Vendor\Package;

use Vendor\Package\{ClassA as A, ClassB, ClassC as C};
use Vendor\Package\SomeNamespace\ClassD as D;
use Vendor\Package\AnotherNamespace\ClassE as E;

use function Vendor\Package\{functionA, functionB, functionC};
use function Another\Vendor\functionD;

use const Vendor\Package\{CONSTANT_A, CONSTANT_B, CONSTANT_C};
use const Another\Vendor\CONSTANT_D;

/**
 * FooBar is an example class.
 */
class FooBar
{
    // ... 其他php代码 ...
}

深度不能超过两层的复合名称空间,因此以下展示了允许的最大复合深度。

<?php

use Vendor\Package\SomeNamespace\{
    SubnamespaceOne\ClassA,
    SubnamespaceOne\ClassB,
    SubnamespaceTwo\ClassY,
    ClassZ,
};

并且不允许以下内容:

<?php

use Vendor\Package\SomeNamespace\{
    SubnamespaceOne\AnotherNamespace\ClassA,
    SubnamespaceOne\ClassB,
    ClassZ,
};

当希望在 PHP 外部包含标记的文件中声明严格类型时打开和关闭标签, 声明必须写在文件的第一行并且包含在一个开始的 PHP 标签,以及严格的类型声明和结束标签。

例如:

<?php declare(strict_types=1) ?>
<html>
<body>
    <?php
        // ... 其他 PHP 代码  ...
    ?>
</body>
</html>

声明语句不能包含空格,并且必须完全是 declare(strict_types=1) (带有可选的分号终止符)。

允许使用块声明语句,并且必须按照以下的格式设置。注意的位置括号和间距:

declare(ticks=1) {
    // 一些代码
}

4、类,属性,和方法

这里的『类』指的是所有类,接口,以及 trait 。

任何注释和语句 不得 跟在其右花括号后的同一行。

当实例化一个类时,后面的圆括号 必须 写出来,即使没有参数传进其构造函数。

new Foo();

4.1 继承和实现

关键字 继承 和 实现 必须 在类名的同一行声明。

类的左花括号 必须 另起一行;右花括号 必须 跟在类主体的下一行。

类的左花括号 必须 独自成行,且 不得 在其上一行或下一行存在空行。

右花括号 必须 独自成行,且 不得 在其上一行存在空行。

<?php

namespace Vendor\Package;

use FooClass;
use BarClass as Bar;
use OtherVendor\OtherPackage\BazClass;

class ClassName extends ParentClass implements \ArrayAccess, \Countable
{
    // 常量,属性,方法
}

如果有接口, 实现 接口和 继承父类 可以 分为多行,前者每行需缩进一次。 当这么做时,第一个接口 必须 写在下一行,且每行 必须 只能写一个接口。

<?php

namespace Vendor\Package;

use FooClass;
use BarClass as Bar;
use OtherVendor\OtherPackage\BazClass;

class ClassName extends ParentClass implements
    \ArrayAccess,
    \Countable,
    \Serializable
{
    // 常量,属性,方法
}

4.2 使用 trait

在类里面用于实现 trait 的关键字 use 必须 在左花括号的下一行声明。

<?php

namespace Vendor\Package;

use Vendor\Package\FirstTrait;

class ClassName
{
    use FirstTrait;
}

每个导入类的 trait 必须 每行一个包含声明,且每个包含声明 必须 有其 use 导入语句。

<?php

namespace Vendor\Package;

use Vendor\Package\FirstTrait;
use Vendor\Package\SecondTrait;
use Vendor\Package\ThirdTrait;

class ClassName
{
    use FirstTrait;
    use SecondTrait;
    use ThirdTrait;
}

在类文件中,如果在使用 ‘use Trait’ 之后没有其他内容了 ,类名右大括号必须另起一行。

<?php

namespace Vendor\Package;

use Vendor\Package\FirstTrait;

class ClassName
{
    use FirstTrait;
}

如有其他内容,两者之间需空一行。

<?php

namespace Vendor\Package;

use Vendor\Package\FirstTrait;

class ClassName
{
    use FirstTrait;

    private $property;
}

当使用 insteadofas 运算符时,它们必须如图所示使用,注意缩进、间距和另起一行。

<?php

class Talker
{
    use A, B, C {
        B::smallTalk insteadof A;
        A::bigTalk insteadof C;
        C::mediumTalk as FooBar;
    }
}

4.3 属性和常量

所有属性 必须 声明可见性。

如果你的项目 PHP 最小版本支持常量可见性( PHP 7.1 或以上),所有常量 必须 声明可见性。

关键字 var 不得 用于声明属性。

每条声明语句 不得 声明多于一个属性。

属性名 不得 用单个下划线开头表明其受保护的或私有的可见性。也就是说,一个下划线开头显然是没有意义的。

类型声明和属性名之间 必须 有一个空格。

一个属性声明看上去如下所示:

<?php

namespace Vendor\Package;

class ClassName
{
    public $foo = null;
    public static int $bar = 0;
}

4.4 方法和函数

所有的方法 必须 事先声明类型。

方法命名 一定不可 用单个下划线来区分是 protected 或 private 类型。也就是说,不要用一个没有意义的下划线开头。

方法和函数名称中,方法命名后面 一定不可 使用空格。方法开始的花括号 必须 写在方法声明后自成一行, 结束花括号也 必须 写在方法后面自成一行。开始左括号后和结束右括号前,都 一定不可 有空格符。

一个方法的声明应该如下所示。注意括号,逗号,空格和花括号的位置:

<?php

namespace Vendor\Package;

class ClassName
{
    public function fooBarBaz($arg1, &$arg2, $arg3 = [])
    {
        // 方法主体
    }
}

一个函数的声明应该如下所示。注意括号,逗号,空格和花括号的位置:

<?php

function fooBarBaz($arg1, &$arg2, $arg3 = [])
{
    // 函数主体
}

4.5 方法和函数参数

在参数列表中, 不得 在每个逗号前存在空格,且 必须 在每个逗号后有一个空格。

方法和函数中带有默认值的参数 必须 放在参数列表的最后。

<?php

namespace Vendor\Package;

class ClassName
{
    public function foo(int $arg1, &$arg2, $arg3 = [])
    {
        // 方法主体
    }
}

参数列表 可以 分为多行,每行参数缩进一次。当这么做时,第一个参数 必须 放在下一行,且每行 必须 只能有一个参数。

当参数列表分成多行时,右圆括号和左花括号 必须 放在同一行且单独成行,两者之间存在一个空格。

<?php

namespace Vendor\Package;

class ClassName
{
    public function aVeryLongMethodName(
        ClassTypeHint $arg1,
        &$arg2,
        array $arg3 = []
    ) {
        // 方法主体
    }
}

当你定义一个返回值类型声明时,冒号后面的类型声明 必须 用空格符隔开。 冒号和声明 必须 在同一行,且跟参数列表后的结束括号之间没有空格。

<?php

declare(strict_types=1);

namespace Vendor\Package;

class ReturnTypeVariations
{
    public function functionName(int $arg1, $arg2): string
    {
        return 'foo';
    }

    public function anotherFunction(
        string $foo,
        string $bar,
        int $baz
    ): string {
        return 'foo';
    }
}

在可空类型声明中,问号和类型声明之间不能有空格。

<?php

declare(strict_types=1);

namespace Vendor\Package;

class ReturnTypeVariations
{
    public function functionName(?string $arg1, ?int &$arg2): ?string
    {
        return 'foo';
    }
}

当在参数之前使用引用运算符 & 时,引用运算符之后不能有空格,例如上面的示例。

可变参数声明的三个点和参数名称之间不能有空格:

public function process(string $algorithm, ...$parts)
{
    // 函数体
}

当同时使用引用运算符和可变参数运算符时,它们之间不能有任何空格:

public function process(string $algorithm, &...$parts)
{
    // 函数体
}

4.6 abstract, final, and static

如果是 abstract and final ,那么申明的时候必须是可见性声明。

如果是 static ,声明必须位于可见性声明之后。

<?php

namespace Vendor\Package;

abstract class ClassName
{
    protected static $foo;

    abstract protected function zim();

    final public static function bar()
    {
        // 请求体
    }
}

4.7 方法和函数的调用

当我们在进行方法或者函数调用的时候,方法名或函数名与左括号之间不能出现空格,在右括号之后也不能出现空格, 并且在右括号之前也不能有空格。在参数列表中,每个逗号前面不能有空格,每个逗号后面必须有一个空格。

<?php

bar();
$foo->bar($arg1);
Foo::bar($arg2, $arg3);

参数列表可以分为多行,每行后面缩进一次。这样做时,列表中的第一项必须位于下一行,并且每一行必须只有一个参数。 跨多个行拆分单个参数 (就像匿名函数或者数组那样) 并不构成拆分参数列表本身。

<?php

$foo->bar(
    $longArgument,
    $longerArgument,
    $muchLongerArgument
);
<?php

somefunction($foo, $bar, [
  // ...
], $baz);

$app->get('/hello/{name}', function ($name) use ($app) {
    return 'Hello ' . $app->escape($name);
});

5、流程控制

如下是主要的流程控制风格规则:

流程控制关键词之后 必须 要有一个空格
左括号后面 不能 有空格
右括号前面 不能 有空格
右括号与左大括号之间 必须 要有一个空格
流程主体 必须 要缩进一次
流程主体 必须 在左大括号之后另起一行
右大括号 必须 在流程主体之后另起一行

每个流程控制主体 必须 以封闭的括号结束。这将标准化流程结构,同时减少由于流程中添加新的内容而引入错误的可能性。

5.1 if, elseif, else

if 结构如下。注意括号,空格,和大括号的位置;elseelseif 都在同一行,和右大括号一样在主体的前面。

<?php

if ($expr1) {
    // if body
} elseif ($expr2) {
    // elseif body
} else {
    // else body;
}

应该 使用关键词 elseif 替换 else if,这样所有的控制关键词看起来都像单个词。

括号中的表达式 可能 会被分开为多行,每一行至少缩进一次。如果这样做,第一个条件 必须 在新的一行。 右括号和左大括号 必须 在同一行,而且中间有一个空格。条件中间的布尔控制符 必须 在每一行的开头或者结尾,而不是混在一起。

<?php

if (
    $expr1
    && $expr2
) {
    // if body
} elseif (
    $expr3
    && $expr4
) {
    // elseif body
}

5.2 switch, case

switch 结构如下。注意括号,空格和大括号的位置。 case 必须 缩进一次,从 switch 开始, break 关键词 (或者其他终止关键词) 必须 缩进和 case 主体保持一致。 必须 要有一个像 // no break 这样的注释在不为空且不需要中断的 case 主体之中。

<?php

switch ($expr) {
    case 0:
        echo 'First case, with a break';
        break;
    case 1:
        echo 'Second case, which falls through';
        // no break
    case 2:
    case 3:
    case 4:
        echo 'Third case, return instead of break';
        return;
    default:
        echo 'Default case';
        break;
}

括号中的表达式 可能 会被分开多行,每一行至少要缩进一次。如果这样做,第一个条件 必须 在新的一行。 右括号和左大括号 必须 在同一行,而且中间有一个空格。条件中间的布尔控制符 必须 在一行的开头或者结尾,而不是混在一起。

<?php

switch (
    $expr1
    && $expr2
) {
    // structure body
}

5.3 while, do while

while 结构如下。注意括号,空格和大括号的位置。

<?php

while ($expr) {
    // structure body
}

括号中的表达式 可能 会被分开多行,每一行至少要缩进一次。如果这样做,第一个条件 必须 在新的一行。 右括号和左大括号 必须 在同一行,而且中间有一个空格。条件中间的布尔控制符 必须 在每一行的开头或者结尾,而不是混在一起。

<?php

while (
    $expr1
    && $expr2
) {
    // structure body
}

同样的, do while 申明如下。注意括号,空格和大括号的位置。

<?php

do {
    // structure body;
} while ($expr);

括号中的表达式 可能 会被分开多行,每一行至少要缩进一次。如果这样做,第一个条件 必须 在新的一行。 条件中间的布尔控制符 必须 在每一行的开头或者结尾,而不是混在一起。

<?php

do {
    // structure body;
} while (
    $expr1
    && $expr2
);

5.4 for

for 申明如下。注意括号,空格和大括号的位置。

<?php

for ($i = 0; $i < 10; $i++) {
    // for body
}

括号中的表达式 可能 会被分开多行,每一行至少要缩进一次。如果这样做,第一个条件 必须 在新的一行。 右括号和左大括号 必须 在同一行,而且中间有一个空格。

<?php

for (
    $i = 0;
    $i < 10;
    $i++
) {
    // for body
}

5.5 foreach

foreach 语句的写法如下所示。请注意它的圆括号、空格和花括号。

<?php

foreach ($iterable as $key => $value) {
    // 迭代主体
}

5.6 try , catch , finally

一个 try-catch-finally 模块包含下面这些内容。请注意它的圆括号、空格和花括号。

<?php

try {
    // try 主体
} catch (FirstThrowableType $e) {
    // 捕获异常主体
} catch (OtherThrowableType | AnotherThrowableType $e) {
    // 捕获异常主体
} finally {
    // finally 主体
}

6、运算符

运算符的样式规则按元数分组(其接受的操作数个数)。

当运算符周围允许出现空格时, 可以 出于可读性目的打多个空格。

所有这里没描述的运算符暂不作限定。

6.1. 一元运算符

递增 / 递减运算符和操作数之间 不得 有任何空格。

$i++;
++$j;

类型转换运算符的圆括号内部 不得 有任何空格:

$intValue = (int) $input;

6.2. 二元运算符

所有二进制 算术,比较,赋值,按位,逻辑、字符串和类型运算符必须在前后跟至少一个空格:

if ($a === $b) {
    $foo = $bar ?? $a ?? $b;
} elseif ($a > $b) {
    $foo = $a + $b * $c;
}

6.3. 三元运算符

条件运算符,也称为三元运算符,必须在 ?: 这两个字符之间:

$variable = $foo ? 'foo' : 'bar';

如果省略条件运算符的中间操作数,运算符必须遵循与其他二进制比较运算符相同的样式规则:

$variable = $foo ?: 'bar';

7、闭包(Closures)

闭包声明时必须在 function 关键字后留有 1 个空格,并且在 use 关键字前后各留有 1 个空格。

左花括号必须跟随前文写在同一行,右花括号必须在函数体后换行放置。

不能在参数和变量的左括号后和右括号前放置空格。

不能在参数和变量的逗号前放置空格,但必须在逗号后放置 1 个空格。

闭包参数如果有默认值,该参数必须放在参数列表末尾。

如果声明了返回类型,它必须遵循普通函数和方法相同的规则;如果使用 use 关键字,冒号必须在 use 右括号后,且冒号前不能有空格。

闭包的声明方式如下,留意括号,逗号,空格和花括号:

<?php

$closureWithArgs = function ($arg1, $arg2) {
    // 函数体
};

$closureWithArgsAndVars = function ($arg1, $arg2) use ($var1, $var2) {
    // 函数体
};

$closureWithArgsVarsAndReturn = function ($arg1, $arg2) use ($var1, $var2): bool {
    // 函数体
};

参数和变量可以分多行放置,每个后续行缩进一次。执行此操作时,列表中的第一项必须放在下一行,并且每行只能有一个参数或变量。

结束多行列表(或者参数,变量)的时候,右括号和左大括号 必须 要放在一行,而且中间有一个空格。

下面是有和没有多行参数列表与变量列表的闭包示例。

<?php

$longArgs_noVars = function (
    $longArgument,
    $longerArgument,
    $muchLongerArgument
) {
   // body
};

$noArgs_longVars = function () use (
    $longVar1,
    $longerVar2,
    $muchLongerVar3
) {
   // body
};

$longArgs_longVars = function (
    $longArgument,
    $longerArgument,
    $muchLongerArgument
) use (
    $longVar1,
    $longerVar2,
    $muchLongerVar3
) {
   // body
};

$longArgs_shortVars = function (
    $longArgument,
    $longerArgument,
    $muchLongerArgument
) use ($var1) {
   // body
};

$shortArgs_longVars = function ($arg) use (
    $longVar1,
    $longerVar2,
    $muchLongerVar3
) {
   // body
};

注意格式化规则也适用一个闭包在一个方法或者操作中作为参数被直接引用。

<?php

$foo->bar(
    $arg1,
    function ($arg2) use ($var1) {
        // body
    },
    $arg3
);

8、匿名类

匿名类 必须 遵循上面章节中和闭包一样的方针和准则。

<?php

$instance = new class {};

只要 implements 接口列表不换行,左花括号 可以 和关键字 class 在同一行。 如果接口列表换行,花括号 必须 放在最后一个接口的下一行。

<?php

// 花括号在同一行
$instance = new class extends \Foo implements \HandleableInterface {
    // 类内容
};

// 花括号在下一行
$instance = new class extends \Foo implements
    \ArrayAccess,
    \Countable,
    \Serializable
{
    // 类内容
};

PSR-13 超媒体链接

在 HTML 和各种 API 格式的上下文中,超媒体链接已经变成 Web 越来越重要的一部分。 然而遗憾的是,没有一种通用单一的超媒体格式,也没有一种通用的方式来表示链接间的格式。

该规范旨在为 PHP 开发人员提供一种简单的、通用的方式来表示一个独立于所使用的序列化格式的超媒体链接。 这反过来又允许系统将超媒体链接的响应序列化为一种或多种有线格式,而不依赖于决定这些链接应该是什么的过程。

为了避免歧义,文档大量使用了「能愿动词」,对应的解释如下:

必须 (MUST):绝对,严格遵循,请照做,无条件遵守。
一定不可 (MUST NOT):禁令,严令禁止。
应该 (SHOULD) :强烈建议这样做,但是不强求。
不该 (SHOULD NOT):强烈不建议这样做,但是不强求。
可以 (MAY) 和可选 (OPTIONAL) :选择性高一点,在这个文档内,此词语使用较少。

参见 RFC 2119

参考文献

RFC 2119
RFC 4287
RFC 5988
RFC 6570
IANA Link Relations Registry
Microformats Relations List

1、规范

1.1 基本链接

一个超媒体链接至少是由这些组成的:

  • 一个 URI 表示目标资源被引用。
  • 一个关系定义如何把目标资源与源联系起来。

链接的各种其他属性可能存在,具体取决于所使用的格式。作为额外的属性将不能标准化和通用,故本规范不寻求规范他们。

就本规范而言,下列定义是适用的:

  • Implementing Object - 通过这个规范一个对象实现一个接口的定义。
  • Serializer - 一个库或者其它系统需要一个或多个 Link 对象,并用它定义的一些格式产生一个序列化的表示。

1.2 属性

所有的链接 可以 包含零个或者多个 URI 和关系之外的附加属性。没有正式的在这里允许的能注册的值和值的有效性取决于上下文, 并且通常取决于一个特定的序列化格式。一般情况下支持的值包括 hreflang、title 和 type。

如果序列化格式需要,序列化 可以 忽略链接对象上的属性。不管怎样,序列化 应该 对所有提供的属性尽可能地进行编码, 以便允许用户扩展,除非有通过序列化格式的定义去阻止的情况。

一些属性(一般为 hreflang)在他们的上下文中 可以 多次出现。因此,一个属性值 可以 是一个数组形式的值而不是一个简单的值。 序列化 可以 对任何适合于序列化格式的格式对该数组进行编码(比如:一个空格分隔的列表,逗号分隔的列表等等)。 如果在一个特定的上下文中,指定的一个属性不允许有多个值,序列化 必须 使用第一个提供的值而忽略所有后续的值。

如果一个属性的值为布尔值 true,则序列化 可以 使用序列化格式支持的和合适的缩写形式。 例如:当属性的存在有布尔意义时,HTML 允许属性没有值。当且仅当该属性为布尔值 true 时,这个规则才适用, 而不适用于 PHP 中的其他任何 truthy 值,例如整数 1。

如果一个属性的值为布尔值 false,序列化 应该 完全省略属性,除非这样做会改变结果的语义含义。 当且仅当该属性为布尔值 false 时,这个规则才适用,而不适用于 PHP 中的其他任何 falsey 值,例如整数 0。

1.3 关系

链接关系定义为字符串,在公开定义关系的情况下为一个简单关键字,或者在私有关系的情况下为一个绝对 URI。

在使用一个简单的关键字的情况下,它 应该 从 IANA 注册表中的一个匹配:

http://www.iana.org/assignments/link-relations/link-relations.xhtml

可以 选择使用 microformats.org 注册表,但这可能不适用于任何情况:

http://microformats.org/wiki/existing-rel-values

一个未在上述其中一个注册表或者一个类似的公共注册表中定义的关系被视为 private, 也就是说,明确到一个特定的应用和使用场景。例如:关系必须使用一个绝对 URI 的情形。

1.4 链接模板

RFC 6570 为 URI 模板定义了一种格式,也就是说,URI 这种模式期望通过客户端工具提供的值去填充。 有些超媒体格式支持模板链接而有些则不支持,并且可能有一种特殊的方式来表示链接是一个模板。 一个不支持 URI 模板格式化程序 必须 忽略它遇到的任何模板的链接。

1.5 可演进的提供者

在某些情况下,一个链接提供者可能需要添加其他链接的能力。在其他情况下,链接提供者必须是只读的, 其中链接在运行时从其他某个数据源衍生。出于这个原因,可修改的提供者可以可选地实现辅助的接口。

另外,一些链接提供者对象,如 PSR-7 响应对象,被设计为不可变的。这意味着在就地添加链接的方法将是不兼容的。 因此,EvolvableLinkProviderInterface 类的单一方法需要返回一个新的copy对象,与原始对象相同,但要包含一个额外的链接对象。

1.6 可演进的链接对象

链接对象在大部分情况下是值对象。因此,允许它们去演进,与 PSR-7 值对象一样是个有用的选项。 为了这个缘故,一个额外的 EvolvableLinkInterface 类被包含进来,它提供了只需一次更改而生成新copy对象实例的方法。 相同的模式被使用在 PSR-7 中,归功于 PHP 的 copy-on-write 机制,使 CPU 和内存依然高效。

然而,模板没有可演进的方法,由于一个链接的模板值是基于专门的 href 值。 它被 禁止 独立地设置,但衍生自 href 值是否为 RFC 6570 的链接模板。

2、包

描述的接口和类作为 psr/link 包的一部分被提供。

3、接口

3.1 Psr\Link\LinkInterface

<?php

namespace Psr\Link;

/**
 * 一个可读的链接对象。
 */
interface LinkInterface
{
    /**
     * 返回链接的目标。
     *
     * 目标链接必须是以下中的一个:
     * 
     * - 一个绝对的 URI,由 RFC 5988 定义的。
     * - 一个相对 URI,由 RFC 5988 定义的。相对链接的基础
     *   被假定为基于客户端的上下文而已知。
     * - 一个由 RFC 6570 定义的 URI 模板。
     *
     * 如果返回一个 URI 模板,isTemplated 必须返回 True。
     *
     * @return string
     */
    public function getHref();

    /**
     * 返回的是否为一个模板链接。
     *
     * @return bool True 表示链接对象是模板, False 相反。
     */
    public function isTemplated();

    /**
     * 返回链接的关系类型。
     *
     * 此方法返回一个链接的 0 个或更多关系类型,返回值为
     * 字符串数组。
     *
     * @return string[]
     */
    public function getRels();

    /**
     * 返回描述目标 URI 的一个属性列表。
     * 
     * @return array
     *  属性的一个键值对列表,其中键是一个字符串,值要么是一个 PHP 原生提供的,要么是 PHP 字符串数组。
     *  如果没有值,必须返回一个空的数组。
     */
    public function getAttributes();
}

3.2 Psr\Link\EvolvableLinkInterface

<?php

namespace Psr\Link;

/**
 * 一个可演进的值对象.
 */
interface EvolvableLinkInterface extends LinkInterface
{
    /**
     * 返回一个指定的 href 实例。
     * 
     * @param string $href
     *  这个 href 值必须包括以下其中一项:
     *   - 一个由 RFC 5988 定义的绝对 URI。
     *   - 一个由 RFC 5988 定义的相对 URI。相对链接的基准假设是由已知客户端基于上下文的。
     *   - 一个由 RFC 6570 定义的 URI 模板。
     *   - 一个实现 __toString() 方法的对象,它产生上述某个值。
     *   
     *   一个实现库应当立即将传递的对象评估为字符串,而不是等待它稍后返回。
     *   
     * @return static
     */
    public function withHref($href);

    /**
     * 返回一个包含指定关系的实例。
     *
     * 如果指定的 rel 已经存在,这个方法必须正常返回而没有错误,但不会再次添加 rel。
     *
     * @param string $rel 要添加的关系值。
     * 
     * @return static
     */
    public function withRel($rel);

    /**
     * 返回一个排除指定关系的实例。
     *
     * 如果指定的 rel 已经不存在,这个方法必须正常返回而没有错误。
     *
     * @param string $rel 要排除的关系值。
     * 
     * @return static
     */
    public function withoutRel($rel);

    /**
     * 返回一个添加了指定属性的实例。
     * 
     * 如果指定的属性已经存在,那么属性的值将被新值覆盖。
     * 
     * @param string $attribute 包含的属性键名。
     * @param string $value 属性待设置的值。
     * 
     * @return static
     */
    public function withAttribute($attribute, $value);

    /**
     * 返回一个排除了指定属性的实例。
     * 
     * 如果指定的属性不存在,这个方法必须正常返回而没有错误。
     * 
     * @param string $attribute 移除的属性键名。
     * 
     * @return static
     */
    public function withoutAttribute($attribute);
}

3.2 Psr\Link\LinkProviderInterface

<?php

namespace Psr\Link;

/**
 * 一个链接提供者对象.
 */
interface LinkProviderInterface
{
    /**
     * 返回一个可迭代的 LinkInterface 对象。
     * 
     * 迭代可能是一个数组或者任何实现 PHP \Traversable 接口的对象。
     * 如果没有可用的链接,一个空的数组或者实现 \Traversable 接口的
     * 对象必须被返回。
     * 
     * @return LinkInterface[]|\Traversable
     */
    public function getLinks();

    /**
     * 返回一个指定关系的可迭代 LinkInterface 对象。
     * 
     * 迭代可能是一个数组或者任何实现 PHP \Traversable 接口的对象。
     * 如果没有与该关系的链接是可用的,一个空的数组或者实现 \Traversable 
     * 接口的对象必须被返回。
     *
     * @return LinkInterface[]|\Traversable
     */
    public function getLinksByRel($rel);
}

3.3 Psr\Link\EvolvableLinkProviderInterface

<?php

namespace Psr\Link;

/**
 * 一个可演进的链接提供者值对象.
 */
interface EvolvableLinkProviderInterface extends LinkProviderInterface
{
    /**
     * 返回一个包含指定链接的实例。
     * 
     * 如果指定的链接已经存在,这个方法必须正常返回而没有错误。
     * 如果 $link 全等于(===)集合中已有的 link 对象,则链接存在。
     *
     * @param LinkInterface $link 应该包含在此集合中的链接对象。
     * 
     * @return static
     */
    public function withLink(LinkInterface $link);

    /**
     * 返回一个移除指定链接的实例。
     * 
     * 如果指定的链接不存在,这个方法必须正常返回而没有错误。
     * 如果 $link 全等于(===)集合中已有的 link 对象,则链接存在。
     *
     * @param LinkInterface $link 移除的链接。
     * 
     * @return static
     */
    public function withoutLink(LinkInterface $link);
}

PSR-13 超媒体链接 - 说明文档

链接定义元文档

1、总结

在 HTML 和各种 API 格式的上下文中,超媒体链接已经变成 Web 越来越重要的一部分。 然而遗憾的是,没有一种通用单一的超媒体格式,也没有一种通用的方式来表示链接间的格式。

该规范旨在为 PHP 开发人员提供一种简单的、通用的方式来表示一个独立于所使用的序列化格式的超媒体链接。 这反过来又允许系统将超媒体链接的响应序列化为一种或多种有线格式,而不依赖于决定这些链接应该是什么的过程。

2、范围

2.1 目标

本规范旨在提取和标准化不同格式之间的超媒体链接表示。

2.2 非目标

本规范不寻求去标准化或偏爱任何特定超媒体序列化的格式

3、设计决策

为什么没有增加方法

本规范的主要目标之一是 PSR-7 响应对象。设计的响应对象必须是不可变的。 其他 value-object 的实现可能也需要一个不可变的接口。

此外,一些链接提供者对象可能不是值对象,而是一个给定域中的其它对象,这个对象能够动态生成链接, 可能是在数据库结果或其他底层表示之外。在这些情况下,与可写的提供者定义是不兼容的。

因此,本规范将访问器方法和可演进方法拆分为单独的接口,允许对象仅根据适合它们的用例去实现只读或可演化的版本。

为什么一个链接对象上的 rel 是多值?

不同的超媒体标准处理具有相同关系的多个链接。有些有一个单一的链接,此链接有多个 rel 的定义。 其它有单一的 rel 条目,然后包含多个链接。

每个链接唯一地定义,但允许它有多个 rel 提供一个最具兼容共性的定义。 一个单独的 LinkInterface 对象可以被序列化为一个或多个链接条目,用一个给定的合适的超媒体格式。 然而,指定多个链接对象,每个链接对象有单个 rel 但具有相同的 URI 也是合法的,并且超媒体格式也可以根据需要对其进行序列化。

为什么需要一个 LinkProviderInterface 接口?

在许多上下文中,一组链接将被系到一些其它的对象上。这些对象可以在所有相关是它们的链接或者它们链接的子集的情形下使用它们。 例如,可以定义代表不同 REST 格式的各种不同的值对象,比如:HAL,JSON-LD 或者 Atom。 从这样的一个对象中均匀地提取这些链接以进行进一步的处理可能是有用的。 例如,从一个对象中提取下一个 / 前一个链接,并将其作为一个链接头添加到 PSR-7 响应 对象上。 另外,许多链接用一个『预负荷』的链接关系表示是有意义的,这将指示一个 HTTP 2 兼容的 Web 服务器, 在预期的后续请求下,应该将链接的资源流传输到客户端。

所有的这些情况都独立于有效负荷和编码对象。通过提供一个公共的接口去访问这样的链接, 我们启用链接自身的通用处理,而不管生成它们的值对象或域对象。

4、相关人

4.1 编辑者

Larry Garfield

4.2 赞助者

Matthew Weier O'Phinney (coordinator)
Marc Alexander

4.3 贡献者

Evert Pot

5、投票

6、相关链接

What's in a link? by Evert Pot
FIG Link Working Group List

PSR-14 事件分发器

事件分发是一种通用的经过良好测试的机制,它允许开发者轻松的,且一致的将逻辑注入到应用中。

PSR 的目的是要为基于事件的扩展和协作建立通用的机制,这样的话,代码库和组件就可以在应用和框架中更自由的复用。

为了避免歧义,文档大量使用了「能愿动词」,对应的解释如下:

  • 必须 (MUST):绝对,严格遵循,请照做,无条件遵守;
  • 一定不可 (MUST NOT):禁令,严令禁止;
  • 应该 (SHOULD) :强烈建议这样做,但是不强求;
  • 不该 (SHOULD NOT):强烈不建议这样做,但是不强求;
  • 可以 (MAY) 和 可选 (OPTIONAL) :选择性高一点,在这个文档内,此词语使用较少;

参见:RFC 2119

目标

有了用于调度和处理事件的通用接口,开发者得以用一种通用的形式创建能够与很多框架和应用交互的代码库。

例如:

  • 当用户没有权限时,禁止保存 / 访问数据的安全框架。
  • 一个通用的全页缓存框架。
  • 继承其他代码库的库,而不用管代码库在什么框架中工作。
  • 用于跟踪应用所有行为的日志包。

定义

  • 事件 Event - 事件是发射器生成的消息。它可以是任意的 PHP 对象。
  • 监听器 Listener - 一个监听器是任意的可调用的 PHP 类或函数,它期待着事件的传递。相同的事件可以传递给零个或多个监听器。 如果有必要,一个监听器可以入队一些其他的异步行为。
  • 发射器 Emitter - 发射器是期待分发事件的任何 PHP 代码,也叫调用代码。它不是由任何特定的数据结构表示的,而是指用例。
  • 分发器 Dispatcher - 分发器是一个服务对象,它的事件对象由发射器提供。分发器负责将事件传递给所有相关的监听器, 但是必须把确定哪些监听器应该响应事件这一步骤委托给监听器提供者去做。
  • 监听器提供者 Listener Provider - 监听器提供者负责确定哪些监听器是与给定事件相关的,但是它不能调用监听器。 一个监听器提供者可能会指定零个或多个相关的监听器。

事件

事件是充当发射器和适当的监听器之间的通信单元的对象。

如果用例要求监听器提供信息给发射器,那事件对象可能是可变的。然而,如果没有这种双向通信要求, 那么建议事件对象定义成不可变的。即,不给对象定义改变对象的方法。

实现者必须假设相同的对象将被传递到所有的监听器。

建议,但不要求事件对象实现无丢失的序列化与反序列化;$event == unserialize(serialize($event))应该为真。 如果合适的话,事件对象可以利用 PHP 的 Serializable 接口,__sleep()__wakeup() 魔术方法或类似的语言的功能。

可终止的事件

一个可终止的事件是事件的特殊案例,它包含了一些额外的方法,这些方法阻止后继监听器被调用。 它是通过实现 StoppableEventInterface 来表示的。

当事件被完成时,实现了 StoppableEventInterface 的事件必须从 isPropagationStopped() 返回真。具体由类的实现者自己决定。 比如,一个事件,它请求调用匹配了相应 ResponseInterface 对象的 PSR-7 RequestInterface 对象, 那它可能有一个 setResponse(ResponseInterface $res) 方法供监听器调用,这方法引起 isPropagationStopped() 返回真。

监听器

一个监听器可以是任何可调用的 PHP 类或函数。监听器必须仅有一个参数,即它响应的事件。监听器应该根据相关用例约束参数类型。 就是说,一个监听器可能使用某接口的类型约束,表示它与任何实现了该接口的事件类型兼容,或与该接口的特定实现兼容。

一个监听器应该返回 void,且应该显示约束它。分发器必须忽略从监听器返回的值。

一个监听器可能将操作委托给其他代码。其中包含一个监听器,这个监听器是运行实际业务逻辑对象的浅包装。

一个监听器可能入队来自事件对象的信息,由调度器、队列服务器或者类似的辅助处理程序进行后续处理。 它也可能将序列化的事件对象入队。但是,也要考虑到并不是所有的事件对象都可以被安全的序列化。 辅助处理程序必须假设对事件对象的任何改变都不会传递给监听器。

分发器

分发器是实现了 EventDispatcherInterface 的服务对象。 它负责为已分发的事件从监听器提供者中获取监听器,并调用与该事件相关的每一个监听器。

  • 分发器必须同步按序调用从监听器提供者获得的监听器。
  • 调用完监听器后,必须返回相同的事件对象。
  • 在所有的监听器执行完之前,必不能返回给发射器。

如果传递了可终止的事件,分发器:

  • 必须在每个监听器调用之前调用事件中的方法 isPropagationStopped()。 如果返回为真,必须立刻将事件返回给发射器,且必不能继续调用监听器。 这就意味着如果传递给分发器的事件在调用 isPropagationStopped() 后总是返回真,将不会有监听器被调用。

分发器应该假设从监听器提供者获取的监听器都是类型安全的。就是说,分发器应该假设调用 $listener($event) 不会产生 TypeError

错误处理

有监听器触发异常或者错误必须阻塞后续监听器的执行。监听器触发的异常或错误必须允许回传到发射器。

分发器可能会记录捕获的异常或错误,及其他操作,但是操作完后必须将重新抛回原始的异常或错误。

监听器提供者

监听器提供者负责确定给定事件与哪些监听器相关,哪些监听器应该被调用。 它可能既要决定哪些监听器是相关的也要根据给定的意义按序返回监听器。可能包括:

  • 允许某种形式的注册机制,以便实现者可以按固定顺序将监听器分配给事件。
  • 根据事件的类型和实现的接口,通过反射派生出一个适用的监听器列表。
  • 提前生成可能在运行时查询的已编译的监听器列表。
  • 实现某种形式的访问控制,以便只有当前用户具有特定权限时才会调用某些监听器。
  • 从事件引用的对象(如实体)中提取某些信息,并对该对象调用预定义的生命周期方法。
  • 使用某些任意逻辑将其职责委托给一个或多个其他监听器提供程序。

监听器提供者应该根据事件的类名来区分事件。也可能视情况根据事件的其他信息来区分。

在确定监听器的适用性时,监听器提供程序必须将父类型与事件本身的类型同等对待。在以下情况下:

class A {}

class B extends A {}

$b = new B();

function listener(A $event): void {};

监听器提供者必须将 listener() 视为 $b 的可选监听器,因为它是类型兼容的,除非有其他的规则阻住它这样做。

对象合成

分发器应该组成一个监听器提供者来确定相关的监听器。建议将监听器实现为与分发器不同的对象,但这不是要求的。

接口

namespace Psr\EventDispatcher;

/**
 * Defines a dispatcher for events.
 */
interface EventDispatcherInterface
{
    /**
     * Provide all relevant listeners with an event to process.
     *
     * @param object $event
     *   The object to process.
     *
     * @return object
     *   The Event that was passed, now modified by listeners.
     */
    public function dispatch(object $event);
}
namespace Psr\EventDispatcher;

/**
 * Mapper from an event to the listeners that are applicable to that event.
 */
interface ListenerProviderInterface
{
    /**
     * @param object $event
     *   An event for which to return the relevant listeners.
     * @return iterable[callable]
     *   An iterable (array, iterator, or generator) of callables.  Each
     *   callable MUST be type-compatible with $event.
     */
    public function getListenersForEvent(object $event) : iterable;
}
namespace Psr\EventDispatcher;

/**
 * An Event whose processing may be interrupted when the event has been handled.
 *
 * A Dispatcher implementation MUST check to determine if an Event
 * is marked as stopped after each listener is called.  If it is then it should
 * return immediately without calling any further Listeners.
 */
interface StoppableEventInterface
{
    /**
     * Is propagation stopped?
     *
     * This will typically only be used by the Dispatcher to determine if the
     * previous listener halted propagation.
     *
     * @return bool
     *   True if the Event is complete and no further listeners should be called.
     *   False to continue calling listeners.
     */
    public function isPropagationStopped() : bool;
}

PSR-15 HTTP 请求处理器

这篇文档描述了 HTTP 服务器的请求处理程序(“请求处理器”)和 HTTP 服务器的中间组件(“中间件”)的常用接口, 比如 PSR-7 或者是随后将要替代的 PSRs。

HTTP 请求处理器是任何一个 web 项目中的基本部分,服务器端的代码接受请求消息,然后处理它,并且生成一个响应信息。 HTTP 中间件就是一种将处理请求和响应过程从应用层分离出来的方法。

这个接口在本篇文章中的描述将会是抽象的请求处理器和中间件。

  • 注:所有提到的 “请求处理器” 和 “中间件” 都是特定于服务器的请求处理的。

本文件中的 “必须”,“不得” ,“需要” ,“应” ,“不应” ,“应该” ,“不应该” ,“推荐” , “可能” 和 “可选” 等能愿动词按照 RFC 2119 中的描述进行解释

参考

PSR-7
RFC 2119

1. 规范

1.1 请求处理器

请求处理器必须是独立的组件来处理请求和创建由 PSR-7 定义的结果响应。

如果请求条件阻止它产生响应,请求处理程序可能会抛出异常。这个异常的类型是没有定义的。

使用此标准的请求处理程序 必须 实现以下接口:

Psr\Http\Server\RequestHandlerInterface

1.2 中间件

中间件组件是一个独立组件,通常与其他中间件组件一起参与处理传入请求并创建由 PSR-7 定义的结果响应。

如果条件充分,中间件组件 应该 创建并返回响应而不委托给请求处理程序。

使用该标准的中间件 必须 实现以下接口:

Psr\Http\Server\MiddlewareInterface

1.3 生成响应

建议任何生成响应的中间件或请求处理程序将组成 PSR-7 ResponseInterface 的原型或能够生成 ResponseInterface 实例的工厂, 以防止依赖于特定的 HTTP 消息实现。

1.4 异常处理

建议任何使用中间件的应用程序都包含捕获异常并将其转换为响应的组件。 这个中间件应该是第一个被执行的组件,并且包含所有进一步的处理以确保始终生成响应。

2. 接口

2.1 Psr\Http\Server\RequestHandlerInterface

下面的接口必须由请求处理程序实现。

namespace Psr\Http\Server;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
 * 处理服务器请求并返回响应
 *
 * HTTP 请求处理程序处理 HTTP 请求,以便生成 HTTP 相应。
 */
interface RequestHandlerInterface
{
    /**
     * 处理服务器请求并返回响应
     *
     * 可以调用其他协助代码来生成响应。
     */
    public function handle(ServerRequestInterface $request): ResponseInterface;
}

2.2 Psr\Http\Server\MiddlewareInterface

以下接口 必须 由兼容的中间件组件来实现。

namespace Psr\Http\Server;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
 * 参与处理服务器的请求与响应
 *
 * 一个 HTTP 中间件组件参与处理一个 HTTP 的消息:
 * 通过对请求进行操作, 生成相应,或者将请求转发给后续的中间件,并  且可能对它的响应进行操作
 * 
 */
interface MiddlewareInterface
{
    /**
     * 处理一个传入的请求
     *
     * 处理传入的服务器请求以产生相应.
     * 如果无法生成响应本身,它可能会委托给提供的请求处理程序来执行此操作
     * 
     */
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface;
}

PSR-16 缓存接口

缓存库公共接口

本文档描述了一个简单也易扩展的接口,针对缓存项目和缓存驱动。

本文档中的关键字「必须」、「不得」、「要求」、「应」、「不应」、「应该」、「不应该」、「推荐」、「可能」、「可选」, 使用 RFC 2119 中的描述进行解释。

最终的实现相比本提议可能会有更多功能,当然它们必须首先实现指明的接口、功能。

1、 规范

1.1 简介

使用缓存是提升项目性能的通用方法,这使得缓存功能成为许多框架和库最常见的功能之一。 如果各个缓存库提供相同的使用接口,意味着库可以丢弃他们自己的缓存实现方式, 然后方便的使用框架中的缓存功能,或者使用其他专门的缓存库。

PSR-6 已经解决了这个问题,但是在一些简单的用例中显得过于繁琐。这个标准为大部分情况构建更加简单的接口标准。 它独立于 PSR-6,但尽可能的兼容 PSR-6。

1.2 定义

  • 调用库,实现库,TTL,过期和 Key 都是从 PSR-6 复制而来,因为他们意义相同。

  • 调用库 - 需要使用缓存服务的库或者代码。调用库会使用实现了该标准(PSR-16)接口的缓存服务,但不必知道这些缓存服务的具体实现方式。

  • 实现库 - 实现库负责实现这个标准(PSR-16),以便为调用库提供缓存服务。 实现库 必须 提供实现了 Psr\SimpleCache\CacheInterface 接口的类。实现库 必须 以整数秒(s)作为缓存有效时长(TTL)的最小粒度。

  • 有效时长(TTL)- 有效时长 (TTL) 是指一个缓存项从存储到过期的时间长度。TTL 一般用以秒为单位的整数或者 DateInterval 实例对象表示。

  • 过期(Expiration) - 过期是指一个缓存项过期的具体时间。它通过缓存项保存时指定的有效时长(TTL)计算得到。 如果一个存储项在 1:30:00 存储,有效时长(TTL)为 300 秒,那么这个存储项会在 1:35:00 过期(Expiration)。

实现库 可以 让一个缓存项提前过期,但缓存项一旦到了过期时间就 必须 作为已过期处理。 如果调用库存储一个缓存项时没有设置过期时间或者有效时长,或者设置为了 null,实现库 可以 指定默认值。 如果没有设置默认值,实现库 必须 把该缓存项设置为永不过期,或者过期时长设置为系统所支持的最大长度。

如果有效时长(TTL)被设置为负数或者 0,该缓存项 必须 从缓存中删除使之失效。

  • Key - 用于指定缓存项唯一性的字符串,至少一个字符长度。实现库 必须 支持由 A-Za-z0-9_. 以任意顺序并使用 UTF-8 编码组成的字符串作为 Key,支持的长度需要达到 64 个字符长度。 实现库 可以 支持额外的字符和字符编码,或者支持更长的字符长度,但上面所说的必须支持。 实现库存储时允许根据需要对 Key 的字符进行转义处理,但 必须 能够返回未经处理过的原始 Key 字符串。 以下字符作为保留字段,实现库 必须不能 使用它们:{}()/\@:

  • 缓存(Cache)- 实现了 Psr\SimpleCache\CacheInterface 接口的对象。

  • 缓存未命中(Cache Misses) - 缓存未命中时会返回 null,因此检查一个缓存项保存的值是否为 null 是不可能的。 这是跟 PSR-6 主要的不同点。

1.3 缓存

如果对一个特定的缓存条目没有指定一个默认的 TTL,实现 可以 提供一个用户指定的机制。 如果未提供用户指定的默认值,则实现 必须 默认为底层实现提供一个允许的最大合法值。 如果底层实现不支持 TTL,则用户指定的 TTL 必须 静默忽略。

1.4 数据

实现库 必须 支持所有序列化的 PHP 数据类型,包括:

  • Strings - 任何 PHP 兼容编码中的任意大小的字符串。
  • Integers - PHP 支持的任何大小的所有整数,高达 64 位的签名。
  • Floats - 所有签名的浮点值。
  • Boolean - True 和 False。
  • Null - 空值(尽管它当从一个未命中的缓存中读取时不能区分)。
  • Arrays - 索引,关联和任意深度的的多维数组。
  • Object - 任何支持像这样 $o == unserialize(serialize($o)) 无损序列化和反序列化的对象。 对象 可以 利用 PHP 的可序列化接口,__sleep()__wakeup() 魔术方法,或者相似语言的功能,如果合适的话。

传递到实现库中的所有数据 必须 完全按照传递的方式返回。这包括变量类型。 也就是说,如果 (int)5 是要保存的值,返回 (string)5 的将是错误的。 实现库 可以 使用 PHP 内置的 serialize ()/unserialize () 方法,但不需要这么做。 与它们兼容被简单用作可接受对象值的基线。

如果由于任何原因无法返回确切要保存的值,实现库 必须 响应缓存未命名而不是损坏的数据。

2、 接口列表

2.1 CacheInterface 接口

缓存接口定义了基于缓存实体的最基本的操作,其包括读写和删除单个缓存项目。

另外该接口还定义了处理多个缓存项目的方法,如:一次性写入,读取,删除多个项目的操作。 当你需要执行大量的读 / 写操作时很有用,仅仅一个单次访问缓存服务器便可执行操作多个项目,从而显著的减少延迟时间。

一个 CacheInterface 实例对应一个拥有单个键命令空间的缓存集合,其等价于在 PSR-6 中的 “Pool”, 不同的 CacheInterface 实例可以被相同的 datastore 支持,但必须有在逻辑上是独立的。

<?php

namespace Psr\SimpleCache;

interface CacheInterface
{
    /**
     * 从缓存中取出值
     *
     * @param string $key     该项在缓存中唯一的key值
     * @param mixed  $default key不存在时,返回的默认值
     *
     * @return mixed 从缓存中返回的值,或者是不存在时的默认值
     *
     * @throws \Psr\SimpleCache\InvalidArgumentException
     *   如果给定的key不是一个合法的字符串时,抛出该异常
     */
    public function get($key, $default = null);

    /**
     * 存储值在cache中,唯一关键到一个key及一个可选的存在时间
     *
     * @param string                 $key   存储项目的key.
     * @param mixed                  $value 存储的值,必须可以被序列化的
     * @param null|int|\DateInterval $ttl   可选项.项目的存在时间,如果该值没有设置,且驱动支持生存时间时,将设置一个默认值,或者驱自行处理。
     *
     * @return bool true 存储成功  false 存储失败
     *
     * @throws \Psr\SimpleCache\InvalidArgumentException
     *  如果给定的key不是一个合法的字符串时,抛出该异常。
     */
    public function set($key, $value, $ttl = null);

    /**
     * 删除指定键值的缓存项
     *
     * @param string $key 指定的唯一缓存key对应的项目将会被删除
     *
     * @return bool 成功删除时返回ture,有其它错误时时返回false
     *
     * @throws \Psr\SimpleCache\InvalidArgumentException
     *   如果给定的key不是一个合法的字符串时,抛出该异常。
     */
    public function delete($key);

    /**
     * 清除所有缓存中的key
     *
     * @return bool 成功返回True.失败返回False
     */
    public function clear();

    /**
     * 根据指定的缓存键值列表获取得多个缓存项目
     *
     * @param iterable $keys   在单次操作中可被获取的键值项
     * @param mixed    $default 如果key不存在时,返回的默认值
     *
     * @return iterable  返回键值对(key=>value形式)列表。如果key不存在,或者已经过期时,返回默认值。
     *
     * @throws \Psr\SimpleCache\InvalidArgumentException
     *  如果给定的keys既不是合法的数组,也不可以被转成数组,或者给得的任何一个key不是一个合法的值时,拖出该异常。
     */
    public function getMultiple($keys, $default = null);

    /**
     * 存储一个键值对形式的集合到缓存中。
     *
     * @param iterable               $values 一系列操作的键值对列表
     * @param null|int|\DateInterval $ttl     可选项.项目的存在时间,如果该值没有设置,且驱动支持生存时间时,将设置一个默认值,或者驱自行处理。
     *
     * @return bool 成功返回True.失败返回False.
     *
     * @throws \Psr\SimpleCache\InvalidArgumentException
     *   如果给定的keys既不是合法的数组,也不可以被转成数组,或者给得的任何一个key不是一个合法的值时,拖出该异常.
     */
    public function setMultiple($values, $ttl = null);

    /**
     *  单次操作删除多个缓存项目.
     *
     * @param iterable $keys 一个基于字符串键列表会被删除
     *
     * @return bool True 所有项目都成功被删除时回true,有任何错误时返回false
     *
     * @throws \Psr\SimpleCache\InvalidArgumentException
     *   如果给定的keys既不是合法的数组,也不可以被转成数组,或者给得的任何一个key不是一个合法的值时,拖出该异常.
     */
    public function deleteMultiple($keys);

    /**
     * 判断一个项目在缓存中是否存在
     *
     * 注意: has()方法仅仅在缓存预热的场景被推荐使用且不允许的活跃     
     * 的应用中场景中对get/set方法使用, 因为方法受竞态条件的限制,当     
     * 你调用has()方法时会立即返回true。另一个脚本可以删除它,使应     
     * 用状态过期。
     * @param string $key 缓存键值
     *
     * @return bool  
     *
     * @throws \Psr\SimpleCache\InvalidArgumentException
     *    如果给定的key不是一个合法的字符串时,抛出该异常.
     */
    public function has($key);
}

2.2 CacheException

<?php

namespace Psr\SimpleCache;

/**
 * 库抛出异常的接口,用于所有类型异常。
 */
interface CacheException
{
}

2.3 InvalidArgumentException

<?php

namespace Psr\SimpleCache;

/**
 * 无效缓存参数异常的接口。
 *
 * 当传递一个无效参数时,必须抛出一个实现了此接口的异常。
 */
interface InvalidArgumentException extends CacheException
{
}

PSR-16 缓存接口 - 说明文档

1、 概要

使用缓存是提升项目性能的通用方法,这使得缓存功能成为许多框架和库最常见的功能之一。 如果各个缓存库提供相同的使用接口,意味着库可以丢弃他们自己的缓存实现方式, 然后方便的使用框架中的缓存功能,或者使用其他专门的缓存库。

2、 为什么要这么麻烦?

PSR-6 已经解决了这个问题,但是在一些简单的用例中显得过于繁琐。这个标准在 PSR-6 基础上构建更加简单的接口标准。

3、 范围

3.1 目标

一个简单的缓存操作接口.
提供有关性能 (往返时间) 原因的多键操作的基本支持.
提供一个能将 PSR-6 实现转换为 PSR 简单缓存的适配器模式.
很有可能将所有的缓存 PSRs 从缓存库公开.

3.2 非目标

解决了所有可能的极端情况,PSR-6 已经做得很好了.

4、 方法

在设计上,实现的方法是贫乏的,因为它只在最简单的场景中使用。它无需被所有的缓冲后端实现,也无需在所有的场景中使用。 它只是在 PSR-6 的基础上提供了简便的封装。

5、 人物

5.1 编辑

Paul Dragoonis (@dragoonis)

5.2 赞助商

Jordi Boggiano (@seldaek) - Composer (Coordinator)
Fabien Potencier (@fabpot) - Symfony

5.3 贡献者

以下名单编写了这个 PSR 缓存标准的草案:

Evert Pot (@evert)
Florin Pățan (@dlsniper)

以下名单是早期的审稿人

Daniel Messenger (@dannym87)

6、 投票

投票入口: groups.google.com/d/topic/php-fig/...
接受投票: groups.google.com/d/msg/php-fig/A8...

7、 相关链接

现存各种缓存实现方案调查 , by @dragoonis

PSR-17 HTTP 工厂

这个文档描述了创建符合 PSR-7 规范的 HTTP 对象的工厂通用标准。

PSR-7 没有包含有关如何创建 HTTP 对象的建议,这导致需要在与 PSR-7 的特定实现无关的组件内创建新 HTTP 对象时会遇到困难。

本文档中概述的接口描述了可以实例化 PSR-7 对象的方法。

本文件中的 必须,不得,需要,应,不应,应该,不应该,推荐,可能 和 可选 等能愿动词按照 RFC 2119 中的描述进行解释。

1、详细描述

HTTP 工厂是可以创建由 PSR-7 定义的 HTTP 对象的方法。HTTP 工厂 必须 实现包中提供的所有对象类型。

2、接口

下面的接口 可能 在一个类中实现,也可以在分开的多个类中实现。

2.1 RequestFactoryInterface

用来创建客户端请求。

namespace Psr\Http\Message;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;

interface RequestFactoryInterface
{
    /**
     * 创建一个新的请求
     *
     * @param string $method 请求使用的 HTTP 方法。
     * @param UriInterface|string $uri 请求关联的 URI。
     */
    public function createRequest(string $method, $uri): RequestInterface;
}

2.2 ResponseFactoryInterface

用来创建响应对象。

namespace Psr\Http\Message;

use Psr\Http\Message\ResponseInterface;

interface ResponseFactoryInterface
{
    /**
     * 创建一个响应对象。
     *
     * @param int $code HTTP 状态码,默认值为 200。
     * @param string $reasonPhrase 与状态码关联的原因短语。如果未提供,实现 **可能** 使用 HTTP 规范中建议的值。
     */
    public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface;
}

2.3 ServerRequestFactoryInterface

用来创建服务端请求。

namespace Psr\Http\Message;

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;

interface ServerRequestFactoryInterface
{
    /**
     * 创建一个服务端请求。
     *
     * 注意服务器参数要精确的按给定的方式获取 - 不执行给定值的解析或处理。
     * 尤其是不要从中尝试获取 HTTP 方法或 URI,这两个信息一定要通过函数参数明确给出。
     *
     * @param string $method 与请求关联的 HTTP 方法。
     * @param UriInterface|string $uri 与请求关联的 URI。
     * @param array $serverParams 用来生成请求实例的 SAPI 参数。
     */
    public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface;
}

2.4 StreamFactoryInterface

为请求和响应创建流。

namespace Psr\Http\Message;

use Psr\Http\Message\StreamInterface;

interface StreamFactoryInterface
{
    /**
     * 从字符串创建一个流。
     *
     * 流 **应该** 使用临时资源来创建。
     *
     * @param string $content 用于填充流的字符串内容。
     */
    public function createStream(string $content = ''): StreamInterface;

    /**
     * 通过现有文件创建一个流。
     *
     * 文件 **必须** 用给定的模式打开文件,该模式可以是 `fopen` 函数支持的任意模式。
     *
     * `$filename` **可能** 是任意被 `fopen()` 函数支持的字符串。
     *
     * @param string $filename 用作流基础的文件名或 URI。
     * @param string $mode 用于打开基础文件名或流的模式。
     *
     * @throws \RuntimeException 如果文件无法被打开时抛出。
     * @throws \InvalidArgumentException 如果模式无效会被抛出。
     */
    public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface;

    /**
     * 通过现有资源创建一个流。
     *
     * 流必须是可读的并且可能是可写的。
     *
     * @param resource $resource 用作流的基础的 PHP 资源。
     */
    public function createStreamFromResource($resource): StreamInterface;
}

在从字符串创建流时接口的实现 应该 使用临时资源。这个方法的 推荐 实现是:

$resource = fopen('php://temp', 'r+');

2.5 UploadedFileFactoryInterface

用来创建上传文件创建流。

namespace Psr\Http\Message;

use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UploadedFileInterface;

interface UploadedFileFactoryInterface
{
    /**
     * 创建一个上传文件接口的对象。
     *
     * 如果未提供大小,将通过检查流的大小来确定。
     *
     * @link http://php.net/manual/features.file-upload.post-method.php
     * @link http://php.net/manual/features.file-upload.errors.php
     *
     * @param StreamInterface $stream 表示上传文件内容的流。
     * @param int $size 文件的大小,以字节为单位。
     * @param int $error PHP 上传文件的错误码。
     * @param string $clientFilename 如果存在,客户端提供的文件名。
     * @param string $clientMediaType 如果存在,客户端提供的媒体类型。
     *
     * @throws \InvalidArgumentException 如果文件资源不可读时抛出异常。
     */
    public function createUploadedFile(
        StreamInterface $stream,
        int $size = null,
        int $error = \UPLOAD_ERR_OK,
        string $clientFilename = null,
        string $clientMediaType = null
    ): UploadedFileInterface;
}

2.6 UriFactoryInterface

为客户端和服务器请求创建 URI。

namespace Psr\Http\Message;

use Psr\Http\Message\UriInterface;

interface UriFactoryInterface
{
    /**
     * 创建一个 URI。
     *
     * @param string $uri 要解析的 URI。
     *
     * @throws \InvalidArgumentException 如果给定的 URI 无法被解析时抛出。
     */
    public function createUri(string $uri = '') : UriInterface;
}

PSR-18 HTTP 客户端

这个文档描述了发送 HTTP 请求和接收 HTTP 响应的共同接口。

本文件中的 「必须」,「不得」,「需要」,「应」,「不应」,「应该」,「不应该」,「推荐」, 「可能」 和 「可选」 等能愿动词按照 RFC 2119 中的描述进行解释。

目标

这个 PSR 的目标就是让开发者能够开发一个与 HTTP 客户端解耦的程序库。 它使得程序库可重用性更高,因为它降低了依赖的数量以及降低了版本冲突的可能性。

第二个目标是 HTTP 客户端可以按照 里氏替换原则 进行替换。这意味着所有的客户端在发送请求时行为都是一样的。

定义

  • 客户端 - 客户端是实现了本规范的一个程序库,它能够发送符合 PSR-7 标准的 HTTP 请求,同时能够返回一个符合 PSR-7 标准的 HTTP 响应给调用方。
  • 调用方 - 调用方就是使用客户端的代码。它不用实现本规范,但是它使用了一个实现了本规范的对象(客户端)。

客户端

客户端是实现了客户端接口 ClientInterface 的对象。

客户端 可能 实现了以下功能:

  • 从提供的 HTTP 请求中发送更改过的那个。例如,将消息体压缩后再发出去。
  • 在 HTTP 响应返回到调用库之前改变它。例如,将请求的消息体解压缩。

如果客户端选择更改 HTTP 请求或 HTTP 响应,它 必须 确保对象保持内部一致。 例如,如果客户端解压缩了消息体,那么它 必须 删除请求头里面的 Content-Encoding 并调整 Content-Length

注意,由于 PSR-7 对象是不可变的,因此调用库 不能 假定传递给 ClientInterface::sendRequest() 的对象与实际发送的 PHP 对象相同。 例如,异常返回的请求对象 可能 和传递给 sendRequest() 的对象不一样,因此不能通过全等号 (===) 进行比较。

客户端 必须 实现以下功能:

  • 重新组装 HTTP 临时响应码(1xx)以便于返回 200 或者更实用的响应码给客户端。

错误处理

客户端一定不能将正确格式的 HTTP 请求或 HTTP 响应当做错误条件。

例如,400 和 500 范围内的响应状态代码不能触发异常,必须正常返回到调用库。

当且仅当客户端完全无法发送 HTTP 请求或者无法将 HTTP 响应解析为 PSR-7 格式响应对象时, 客户端才必须抛出 Psr\Http\Client\ClientExceptionInterface 实例。

如果由于请求体不是正常的 HTTP 请求或缺失某些关键信息(例如主机地址或方法名称)而无法发送请求, 客户端必须抛出 Psr\Http\Client\RequestExceptionInterface 实例。

如果由于任何类型的网络故障(包括超时)而无法发送请求, 则客户端必须抛出一个 Psr\Http\Client\NetworkExceptionInterface 实例。

在按照上面定义去实现适当的接口前提下, 客户端可能抛出比这里定义更具体的异常(例如超时异常 TimeOutException 或主机不存在异常 HostNotFoundException)。

接口

客户端接口

namespace Psr\Http\Client;

use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

interface ClientInterface
{
    /**
     * 发送一个 PSR-7 标准的请求,返回一个 PSR-7 格式的响应.
     *
     * @param RequestInterface $request  
     *
     * @return ResponseInterface
     *
     * @throws \Psr\Http\Client\ClientExceptionInterface 发生错误将抛出客户端异常接口对象
     * 
     */
    public function sendRequest(RequestInterface $request): ResponseInterface;
}

客户端异常接口

namespace Psr\Http\Client;

/**
 * 每个HTTP客户端相关的异常都必须实现此接口.
 */
interface ClientExceptionInterface extends \Throwable
{
}

请求异常接口

namespace Psr\Http\Client;

use Psr\Http\Message\RequestInterface;

/**
 * 请求失败时的异常.
 *
 * 举例:
 *      - 请求无效 (e.g. 参数缺失)
 *      - 请求运行错误 (e.g. 响应体不可见)
 */
interface RequestExceptionInterface extends ClientExceptionInterface
{
    /**
     * 获取请求对象.
     * 
     * 请求对象可能和客户端接口发送的对象不一致.
     *
     * @return RequestInterface
     */
    public function getRequest(): RequestInterface;
}

网络异常接口

namespace Psr\Http\Client;

use Psr\Http\Message\RequestInterface;

/**
 * 因网络原因导致请求无法完成时抛出该异常.
 *
 * 抛出该异常将没有响应体,因为收不到响应体时也会抛出这个异常.
 *
 * 举例:域名不能解析或连接失败.
 */
interface NetworkExceptionInterface extends ClientExceptionInterface
{
    /**
     * 返回请求对象.
     *
     * 返回的请求对象可能和客户端接口发送的对象不一致.
     *
     * @return RequestInterface
     */
    public function getRequest(): RequestInterface;
}

参考资料

PHP Standards Recommendations https://www.php-fig.org/psr/

PHP PSR 标准规范 https://learnku.com/docs/psr

PSR-0: Autoloading Standard https://www.php-fig.org/psr/psr-0/

PSR-1 基础编码规范 https://laravel-china.org/topics/2078

PEAR 编码准则 https://pear.php.net/manual/en/standards.php

Symfony 编码准则 https://symfony.com/doc/current/contributing/code/standards.html

PHP中PSR-[0-4]规范:http://m.blog.csdn.net/article/details?id=42844391

PHP中PSR-[0-4]规范:https://www.zybuluo.com/phper/note/65033

php trait 冲突的解决 insteadof https://blog.csdn.net/zmzwll1314/article/details/91417628

视频 psr标准之http消息接口和guzzle深入介绍 https://play.tudou.com/v_show/id_XMjgwNDA2MzIwNA==.html

Psr7实现包 guzzle/guzzle https://github.com/guzzle/guzzle

httpbin http://www.httpbin.org/

视频 PHP PSR 标准规范 26课合集 https://www.bilibili.com/video/BV1z4411R71n?p=1


返回