本文档中的关键词”必须”、”禁止”、”应该”、”不应该”、”可以”、”可选”是按照 RFC 2119 中的描述进行解释。
自动加载
自动加载器通过将命名空间映射到文件系统路径来消除包含文件的复杂性。
PSR-4:自动加载
- 概述
此 PSR 描述了从文件路径自动加载类的规范。它是完全可互操作的,并且可以与任何其他自动加载规范一起使用,包括PSR-0。此 PSR 还描述了放置将根据规范自动加载的文件的位置。
- 规范
2.1 术语“类”是指类、接口、特征和其他类似结构。
2.2 完全限定类名具有以下形式:
\<NamespaceName>(\<SubNamespaceNames>)*\<ClassName>
- 完全限定类名必须有一个顶级命名空间名称,也称为“供应商命名空间”。
- 完全限定的类名可以有一个或多个子命名空间名称。
- 完全限定类名必须有一个最终类名。
- 下划线在完全限定类名的任何部分都没有特殊含义。
- 完全限定类名中的字母字符可以是小写和大写的任意组合。
- 所有类名必须以区分大小写的方式引用。
2.3 加载与完全限定类名相对应的文件时…
- 一个或多个前导命名空间和子命名空间名称的连续系列,不包括前导命名空间分隔符,在完全限定类名(“命名空间前缀”)中至少对应一个“基本目录”。
- “命名空间前缀”之后的连续子命名空间名称对应“基本目录”中的子目录,其中命名空间分隔符表示目录分隔符。子目录名称必须与子命名空间名称的大小写匹配。
- 最终类名对应于以
.php结尾的文件名。文件名必须与最终类名的大小写匹配。
2.4 自动加载器实现不得抛出异常,不得引发任何级别的错误,也不得返回值。
- 示例
下表显示了完全限定类名、命名空间前缀和基本目录的相应文件路径。
| 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:自动加载 - 说明文档
- 概述
目的是为可互操作的 PHP 自动加载器指定规则,将命名空间映射到文件系统路径,并且可以与任何其他 SPL 注册的自动加载器共存。这将是对 PSR-0 的补充,而不是替代。
- 为什么需要它?
PSR-0 的历史
PSR-0 类命名和自动加载标准源于在 PHP 5.2 及之前版本的约束下对 Horde/PEAR 约定的广泛接受。按照这种约定,倾向于将所有 PHP 类放在一个主目录中,在类名中使用下划线表示伪命名空间,如下所示:
/path/to/src/VendorFoo/Bar/Baz.php # VendorFoo_Bar_BazVendorDib/Zim/Gir.php # Vendor_Dib_Zim_Gir
随着 PHP 5.3 的发布和新命名空间的广泛使用,PSR-0 被引入以允许旧的 Horde/PEAR 下划线模式和新命名空间符号的使用。类名中仍然允许使用下划线,以简化从旧命名空间到新命名空间的转换,从而鼓励更广泛的采用。
/path/to/src/VendorFoo/Bar/Baz.php # VendorFoo_Bar_BazVendorDib/Zim/Gir.php # VendorDib_Zim_GirIrk_Operation/Impending_Doom/V1.phpV2.php # Irk_Operation\Impending_Doom\V2
PEAR 安装程序将 PEAR 包中的源文件移动到单个主目录中,这一事实对这种结构非常有影响。
Composer 来了
使用 Composer,包源不再复制到单个全局位置。它们是从安装位置使用的,不会四处移动。这意味着 Composer 没有像 PEAR 那样用于 PHP 源代码的“单一主目录”。相反,有多个目录;每个包都位于每个项目的单独目录中。
为了满足 PSR-0 的要求,这导致 Composer 包看起来像这样:
vendor/vendor_name/package_name/src/Vendor_Name/Package_Name/ClassName.php # Vendor_Name\Package_Name\ClassNametests/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\ClassNametests/ClassNameTest.php # Vendor_Name\Package_Name\ClassNameTest
这将需要实现最初称为“面向包的自动加载”(与传统的“直接类到文件自动加载”对应)。
面向包的自动加载
很难通过扩展或修改 PSR-0 来实现面向包的自动加载,因为 PSR-0 不允许类名的任何部分之间存在中间路径。这意味着面向包的自动加载器的实现将比 PSR-0 更复杂。但是,它将使扩展包更加简洁。
最初,提出了以下规则:
- 实现者必须使用至少两个命名空间级别:供应商名称和供应商包名称。(这种顶级的两个名称组合在下文中称为 vendor-package 名称或 vendor-package 命名空间。)
- 实现者必须允许在 vendor-package 命名空间和完全限定类名的其余部分之间存在路径中缀。
- vendor-package 命名空间可以映射到任何目录。完全限定类名的其余部分必须将命名空间名称映射到同名目录,并且必须将类名映射到以 .php 结尾的同名文件。
请注意,这意味着类名中下划线作为目录分隔符的结尾。有人可能认为应该尊重下划线,因为它们在 PSR-0 下,但鉴于它们在该文档中的存在是指从 PHP 5.2 和以前的伪命名空间过渡,因此也可以在此处删除它们。
- 范围
目标
- 保留 PSR-0 规则,即实现者必须使用至少两个命名空间级别:供应商名称和供应商包名称。
- 允许在 vendor-package 命名空间和完全限定类名的其余部分之间使用路径中缀。
- 允许供应商包命名空间可以映射到任何目录,可能是多个目录。
- 结束将类名中的下划线作为目录分隔符。
非目标
- 提供非类资源的通用变换算法。
- 方案
选择的方法
这种方法保留了 PSR-0 的关键特性,同时消除了它所需的更深层次的目录结构。此外,它还指定了某些附加规则,使实现更明确地具有互操作性。
尽管与目录映射无关,但最终草案还指定了自动加载器应如何处理错误。具体来说,它禁止抛出异常或引发错误。原因有两个。
- PHP 中的自动加载器被明确设计为可堆叠的,因此如果一个自动加载器无法加载一个类,另一个有机会这样做。让自动加载器触发中断错误条件违反了该兼容性。
class_exists()和interface_exists()允许“未找到,即使在尝试自动加载后”作为合法的正常用例。抛出异常的自动加载器会导致class_exists()无法使用,从互操作性的角度来看,这是完全不可接受的。希望在未找到类的情况下提供额外调试信息的自动加载程序应该通过日志记录来实现,无论是到 PSR-3 兼容的记录器还是其他方式。
优点:
- 较浅的目录结构
- 更灵活的文件位置
- 停止将类名中的下划线视为目录分隔符
- 使实现更明确地可互操作
缺点:
- 不再像在 PSR-0 下那样,仅检查类名以确定它在文件系统中的位置(从 Horde/PEAR 继承的“类到文件”约定)。
替代方案:仅使用 PSR-0
只保留 PSR-0,虽然合理,但确实给我们留下了相对更深的目录结构。
优点:
- 无需改变任何人的习惯或实现
缺点:
- 给我们留下更深的目录结构
- 让我们在类名中使用下划线作为目录分隔符
替代方案:拆分自动加载和转换
Beau Simensen 和其他人建议,可以将转换算法从自动加载提案中分离出来,以便其他提案可以引用转换规则。在完成了将它们分开的工作、随后的投票和一些讨论之后,组合版本(即嵌入在自动加载器提案中的转换规则)被揭示为偏好。
优点:
- 转换规则可以被其他提案单独引用
缺点:
- 不符合民意调查对象和部分合作者的意愿
替代方案:使用更多命令式和叙述性语言
在听到多个 +1 选民支持该想法但不同意(或理解)提案的措辞后,发起人撤回了第二次投票后,有一段时间将已投票的提案扩大为更大的叙述和更命令性的语言。这种方法遭到少数参与者的强烈谴责。一段时间后,Beau Simensen 开始针对 PSR-0 进行实验性修订;编辑和赞助商赞成这种更简洁的方法,并主导了目前正在考虑的版本,由 Paul M. Jones 撰写并由许多人贡献。
与 PHP 5.3.2 及以下版本的兼容性说明
5.3.3 之前的 PHP 版本不去除前导命名空间分隔符,因此注意这一点的责任落在了实现上。未能剥离前导命名空间分隔符可能会导致意外行为。
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;}});
类示例
以下是处理多个命名空间的示例类实现:
<?phpnamespace 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');** The following line would cause the autoloader to attempt to load the* \Foo\Bar\Qux\Quux class from /path/to/packages/foo-bar/src/Qux/Quux.php:** <?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;}}
单元测试
以下示例是对上述类加载器进行单元测试的一种方法:
<?phpnamespace 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(){$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-3:日志接口
本文档描述了日志库的通用接口。
主要目标是允许库以简单且通用的方式接收 Psr\Log\LoggerInterface 对象并向其写入日志。有自定义需求的框架和 CMS 可以为自己的需求扩展接口,但应该与本文档保持兼容。这确保了应用程序使用的第三方库可以写入集中的应用程序日志。
本文档中的词implementor将被解释为LoggerInterface在与日志相关的库或框架中实现的人。记录器的用户称为user.
- 规范
基础知识
LoggerInterface公开了八种方法来将日志写入八个 RFC 5424 级别(debug、 info、 notice、 warning、 error、 critical、 alert、 emergency )。- 第九种方法,
log接受日志级别作为第一个参数。使用日志级别常量之一调用此方法必须与调用特定于级别的方法具有相同的结果。如果实现不知道哪个级别,则使用本规范未定义的级别调用此方法,必须抛出一个Psr\Log\InvalidArgumentException异常。用户不应该在不确定当前实现是否支持的情况下使用自定义级别。
消息
- 每个方法都接受一个字符串作为消息,或者一个带有
__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);
上下文
- 每个方法都接受一个数组作为上下文数据。这是为了保存任何不适合字符串的无关信息。该数组可以包含任何内容。实施者必须确保他们尽可能正确处理上下文数据。上下文中的给定值不得抛出异常或引发任何 php 错误、警告或通知。
- 如果一个
Exception对象在上下文数据中传递,它必须在'exception'键中。记录异常是一种常见模式,这允许实现者在日志后端支持时从异常中提取堆栈跟踪。实现者必须在使用它之前验证'exception'键值是否真的是一个Exception,因为它可能包含任何东西。
辅助类和接口
Psr\Log\AbstractLogger类使您可以通过继承它并实现log方法,非常轻松地实现LoggerInterface接口。其他八种方法是将消息和上下文转发给它。- 同样,使用
Psr\Log\LoggerTrait也需要您实现log方法。请注意,由于 trait 不能实现接口,在这种情况下您仍然必须实现LoggerInterface。 - 如果没有可用的记录器,
Psr\Log\NullLogger可以为使用者提供一个备用的“黑洞”实现。但是,如果上下文数据创建成本很高,则条件日志记录可能是更好的方法。 Psr\Log\LoggerAwareInterface唯一包含一个setLogger(LoggerInterface $logger)方法,框架可以使用它来自动连接带有记录器的任意实例。Psr\Log\LoggerAwareTraittrait 可用于在任何类中轻松实现等效接口。它使您可以访问$this->logger。Psr\Log\LogLevel类保存八个日志级别的常量。
- 包
接口和类的描述以及相关的异常类和用于验证您实现的测试套件作为 psr/log 包的一部分提供。
- 接口
**Psr\Log\LoggerInterface**
<?phpnamespace 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());}
**Psr\Log\LoggerAwareInterface**
<?php
namespace Psr\Log;
/**
* logger-aware 定义实例
*/
interface LoggerAwareInterface
{
/**
* 设置一个日志记录实例
*
* @param LoggerInterface $logger
* @return void
*/
public function setLogger(LoggerInterface $logger);
}
**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-3:日志接口 - 说明文档
- 概述
logger 接口定义了一个通用接口,用于记录来自应用程序或库的系统消息。
该说明文档是后来编写的,因为 PSR-3 最初是在说明文档成为标准实践之前通过的。
- 设计决策
静态日志消息
本规范的意图是传递给日志记录方法的消息始终是静态值。任何特定于上下文的可变性(例如用户名、时间戳或其他信息)都应该通过 $context 数组提供,并且字符串应该使用占位符来引用它。
这种设计的意图是双重的。第一,该消息随后可随时用于翻译系统以创建日志消息的本地化版本。第二,特定于上下文的数据可能包含用户输入,因此需要转义。如果日志消息存储在数据库中以便稍后以 HTML 呈现、序列化为 JSON、序列化为 syslog 消息字符串等,则转义必然会有所不同。日志记录实现的责任是确保显示的$context数据用户被适当地转义。
PSR-6:缓存接口
缓存是提高任何项目性能的常用方法,使缓存库成为许多框架和库的最常见功能之一。这导致了许多框架和库推出专属的、功能多样的缓存库。这些差异导致开发人员必须学习多个系统,这些系统可能提供也可能不提供他们需要的功能。此外,缓存库的开发者自己也面临着在仅支持有限数量的框架或创建大量适配器类之间做出选择。
缓存系统的通用接口将解决这些问题。库和框架开发人员可以依靠缓存系统以他们期望的方式工作,而缓存系统的开发人员只需实现一组接口而不是一整套适配器。
- 目标
此 PSR 的目标是允许开发人员创建可以集成到现有框架和系统中的缓存库,而无需进行自定义开发。
- 定义
- 调用库 - 实际需要缓存服务的库或代码。该库负责调用缓存服务,但不需要知道这些缓存服务的具体实现。
- 实施库 - 该库负责具体实现,以便为任何调用库提供缓存服务。实现库必须提供实现
Cache\CacheItemPoolInterface和Cache\CacheItemInterface接口的类。实现库必须至少支持 TTL 功能。 - TTL - 缓存项的生存时间 (TTL) 是从存储该缓存项到它被认为是过期的时间量。TTL 通常由表示时间的整数(以秒为单位)或 DateInterval 对象定义。
- Expiration - 缓存项设置为过时的实际时间。这通常通过将 TTL 添加到存储对象的时间来计算,但也可以使用 DateTime 对象显式设置。在 1:30:00 存储 300 秒 TTL 的缓存项将在 1:35:00 到期。实施库可以在缓存项请求的到期时间之前使缓存项到期,但一旦达到其到期时间,必须将缓存项视为已过期。如果调用库要求保存缓存项但未指定过期时间,或指定空过期时间或 TTL,则实施库可以使用配置的默认持续时间。如果没有设置默认持续时间,实现库必须将其解释为永久缓存项,或者按照底层驱动能支持的最长时间作为持续时间。
- Key - 一个至少包含一个字符的字符串,用于唯一标识缓存项。实现库必须支持由字符
A-Z、a-z、0-9、_和.UTF-8 编码的任意顺序组成的键,并且长度最多为 64 个字符。实现库可以支持额外的字符和编码或更长的长度,但必须至少支持该最小值。库负责自己对密钥字符串进行适当的转义,但必须能够返回原始未修改的密钥字符串。以下字符为将来的扩展保留,实现库不得支持:{}()/\@: - 命中 - 当调用库通过键请求缓存项并且找到该键的匹配值,并且该值尚未过期,并且该值并非由于某些其他原因而无效时,就会发生缓存命中。调用库应该确保在所有 get() 调用上验证 isHit()。
- Miss - 缓存未命中与缓存命中相反。当调用库通过键请求缓存项并且未找到该键的该值,或者该值已找到但已过期,或该值由于某些其他原因无效时,会发生缓存未命中。过期值必须始终被视为缓存未命中。
- 延迟 - 延迟缓存保存表示缓存项可能不会立即被池持久化。池对象可以延迟持久化缓存项,以利用某些存储引擎支持的批量集操作。池必须确保任何延迟的缓存项最终都被持久化并且数据不会丢失,并且可以在调用库请求它们被持久化之前持久化它们。当调用库调用 commit() 方法时,必须保留所有未完成的延迟缓存项。实现库可以使用任何适当的逻辑来确定何时持久化延迟缓存项,例如对象析构函数、在 save() 上持久化所有、超时或最大数量检查或任何其他适当的逻辑。对已延迟缓存项的请求必须返回已延迟但尚未持久化的缓存项。
- 数据
实现库必须支持所有可序列化的 PHP 数据类型,包括:
- 字符串 - 任何 PHP 兼容编码中任意大小的字符串。
- 整数 - PHP 支持的任何大小的所有整数,最多 64 位有符号。
- 浮点数 - 所有带符号的浮点值。
- 布尔值 - 真假。
- Null - 实际的空值。
- 数组 - 任意深度的索引、关联和多维数组。
- 对象 - 任何支持无损序列化和反序列化的对象,例如
$o == unserialize(serialize($o))。对象可以利用 PHP 的序列化接口、__sleep()或__wake等魔术方法,或者其他可用的类似方法(如果合适)。
传递到实现库的所有数据必须完全按照传递的方式返回。这包括变量类型。也就是说,如果 (int) 5 是保存的值,则返回 (string) 5 是错误的。实现库可以在内部使用 PHP 的 serialize()/unserialize()函数,当然也不强制要求这样做。兼容这些的一个基本准线就是接受对象类型。
如果由于任何原因无法返回准确的保存值,实现库必须响应缓存未命中而不是损坏的数据。
- 关键概念
缓冲池 Pool
Pool 表示缓存系统中的缓存项集合。池是它包含的所有缓存项的逻辑存储库。所有可缓存的缓存项都作为 Item 对象从 Pool 中检索,并且与整个缓存对象的所有交互都通过 Pool 发生。
缓存项 Items
Item 表示 Pool 中的单个键/值对。键是项目的主要唯一标识符,并且必须是不可变的。该值可以随时更改。
- 错误处理
虽然缓存通常是应用程序性能的重要组成部分,但它绝不应该是应用程序功能的关键部分。因此,缓存系统中的错误不应导致应用程序失败。出于这个原因,实现库绝不能抛出接口定义的异常,并且应该捕获由底层数据存储触发的任何错误或异常,并且不允许它们冒泡。
实施库应记录此类错误或以其他方式将其报告给适当的管理员。
如果调用库请求删除一个或多个缓存项,或者清除池,如果指定的键不存在,则不得将其视为错误条件。后置条件相同(key不存在,或者pool为空),因此不存在错误条件。
- 接口
CacheItemInterface
CacheItemInterface 定义缓存系统中的缓存项。每个 Item 对象必须与一个特定的键相关联,该键可以根据实现系统进行设置,并且通常由Cache\CacheItemPoolInterface 对象传递。Cache\CacheItemInterface对象封装了缓存项的存储和检索。每个Cache\CacheItemInterface都由一个 Cache\CacheItemPoolInterface对象生成,该对象负责任何所需的设置以及将对象与唯一的 Key 相关联。 Cache\CacheItemInterface对象必须能够存储和检索本文档数据部分中定义的任何类型的 PHP 值。
调用库不得实例化 Item 对象本身。它们只能通过getItem()方法从 Pool 对象中请求。调用库不应该假设由一个实现库创建的项目与来自另一个实现库的池兼容。
<?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:缓存接口 - 说明文档
- 概述
缓存是提高任何项目性能的常用方法,使缓存库成为许多框架和库的最常见功能之一。这导致了许多框架和库推出专属的、功能多样的缓存库。这些差异导致开发人员必须学习多个系统,这些系统可能提供也可能不提供他们需要的功能。此外,缓存库的开发者自己也面临着在仅支持有限数量的框架或创建大量适配器类之间做出选择。
- 为什么需要它?
缓存系统的通用接口将解决这些问题。库和框架开发人员可以依靠缓存系统以他们期望的方式工作,而缓存系统的开发人员只需实现一组接口而不是一整套适配器。
此外,这里介绍的实现是为未来的可扩展性而设计的。它允许各种内部不同但与 API 兼容的实现,并为以后的 PSR 或特定实现者的未来扩展提供了一条清晰的路径。
优点:
- 用于缓存的标准接口允许独立库轻松支持中间数据的缓存;他们可能只是(可选的)依赖这个标准接口并利用它而不关心实现细节。
- 多个项目共享的共同开发的缓存库,即使它们扩展了这个接口,也可能比十几个单独开发的实现更健壮。
缺点:
- 任何接口标准化都有扼杀未来创新的风险,被认为不应该这样实现。但是,我们认为缓存是一个充分商业化的问题场景,此处提供的扩展功能可以减轻任何潜在的停滞风险。
- 范围
目标
- 底层和中间级缓存需求的通用接口。
- 扩展规范以支持高级功能的清晰机制,无论是未来的 PSR 还是单独的实现。这种机制必须允许多个独立的扩展而不会发生冲突。
非目标
- 与所有现有缓存实现的架构兼容性。
- 少数用户使用的高级缓存功能,例如命名空间或标记。
- 方案
选择的方法
本规范采用“存储模型”或“数据映射器模型”进行缓存,而不是更传统的“可过期键值”模型。主要原因是灵活性。一个简单的键/值模型很难扩展。
这里的模型要求使用表示缓存项的 CacheItem 对象和表示缓存数据的给定存储的 Pool 对象。从池中检索缓存项,与之交互并返回给它。虽然有时有点冗长,但它提供了一种良好、健壮、灵活的缓存方法,特别是在缓存比简单地保存和检索字符串更多的情况下。
大多数方法名称是根据对成员项目和其他流行的非成员系统的调查中的常见做法和方法名称选择的。
优点:
- 灵活且可扩展
- 在不违反接口的情况下允许大量的实现变化
- 不会将对象构造函数隐式公开为伪接口。
缺点:
- 比简单的方法更冗长
替代方案:“弱项”方法
早期的各种草案采用了更简单的“key value with expires”方法,也称为“weak item”方法。在这个模型中,“缓存项”对象实际上只是一个带有方法的数组对象。用户将直接实例化它,然后将其传递给缓存池。虽然更熟悉,但该方法有效地阻止了缓存项的任何有意义的扩展。它有效地使缓存项的构造函数成为隐式接口的一部分,从而严重限制了可扩展性及灵活性的能力。
在 2013 年 6 月进行的一项民意调查中,大多数参与者明显倾向于采用更稳健但不那么传统的“强项”/存储库方法,这是前进的方向。
优点:
- 更传统的方法。
缺点:
- 可扩展性或灵活性较差。
替代方案:“裸值”方法
缓存规范的一些最早讨论建议一起跳过缓存项的概念,而只是读取/写入要缓存的原始值。虽然更简单,但有人指出,无法区分缓存未命中和选择表示缓存未命中的任何原始值之间的区别。也就是说,如果缓存查找返回 NULL,则无法判断是否没有缓存值或 NULL 是否是已缓存的值。(在许多情况下,NULL 是一个合法的缓存值。)
我们回顾的大多数更健壮的缓存实现——特别是 Stash 缓存库和 Drupal 使用的本土缓存系统——至少使用 get 某种结构化对象以避免未命中和标记值之间的混淆。根据之前的经验,FIG 决定裸值是不可能的。
替代方案:ArrayAccess 池
有人建议让 Pool 实现 ArrayAccess,这将允许缓存 get/set 操作使用数组语法。这被拒绝了,因为兴趣有限,该方法的灵活性有限(使用默认控制信息进行简单的获取和设置是可能的),并且因为如果希望将特定实现作为附加组件包含在内是微不足道的。
PSR-11:容器接口
本文档描述了依赖注入容器的通用接口。
设定ContainerInterface的目标是标准化框架和库如何使用容器来获取对象和参数(在本文档的其余部分中称为实体)。
本文档中的词implementor被解释为ContainerInterface在依赖注入相关的库或框架中实现的人。依赖注入容器 (DIC) 的用户称为user。
- 规范
基础知识
实体标识符
实体标识符是至少一个字符的任何 PHP 合法字符串,用于唯一标识容器中的对象。实体标识符是一个不透明的字符串,因此调用者不应假定该字符串的结构带有任何语义含义。
从容器中读取
Psr\Container\ContainerInterface公开了两种方法:get和has。get接受一个必传参数:一个实体标识符,它必须是一个字符串。get可以返回任何东西,如果容器没有标识符,则抛出NotFoundExceptionInterface异常。使用相同标识符的两次连续调用get应该返回相同的值。但是,根据implementor实现或user配置,可能会返回不同的值,因此user不应依赖于在两次连续调用中获得相同的值。has接受一个唯一参数:一个实体标识符,它必须是一个字符串。 如果容器已存在实体标识符,则has必须返回true,否则返回false。如果调用has($id)返回了false,那么相同 $id 调用get($id)方法一定要抛出NotFoundExceptionInterface异常。
异常
容器直接抛出的异常应该实现 Psr\Container\ContainerExceptionInterface 。get对具有不存在 id 的方法调用必须抛出一个 Psr\Container\NotFoundExceptionInterface 。
推荐用法
用户不应该将容器传递给对象,以便对象可以检索自己的依赖项。这意味着容器被用作服务定位器 ,这是一种通常不鼓励使用的模式。
- 包
接口和类的描述以及相关异常作为 psr/container 包的一部分提供。
实现 PSR 容器接口的包应该申明为 psr/container-implementation 1.0.0 包。
需要使用容器的项目只需要引入上面实现的包 psr/container-implementation 1.0.0 即可。
- 接口
**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);
}
**Psr\Container\ContainerExceptionInterface**
<?php
namespace Psr\Container;
/**
* 容器中的基础异常类
*/
interface ContainerExceptionInterface
{
}
**Psr\Container\NotFoundExceptionInterface**
<?php
namespace Psr\Container;
/**
* 容器中没有查找到对应对象时的异常
*/
interface NotFoundExceptionInterface extends ContainerExceptionInterface
{
}
PSR-11:容器接口 - 说明文档
- 概述
本文档描述了容器 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 容器都在满足相同的需求:它们为应用程序提供了一种检索一组配置对象(通常是服务)的方法。
通过标准化从容器中获取条目的方式,使用 Container PSR 的框架和库可以与任何兼容的容器一起使用。这将允许最终用户根据自己的喜好选择自己的容器。
- 范围
目标
Container PSR 设定的目标是标准化框架和库如何使用容器来获取对象和参数。
区分容器的两种用法很重要:
- 配置对象实例
- 获取对象实例
大多数时候,这两个方法不被同时使用。虽然通常是最终用户倾向于配置对象,但通常是框架获取对象以构建应用程序。
这就是为什么这个接口只关注如何从容器中获取对象。
非目标
如何在容器中设置对象以及如何配置它们超出了本 PSR 的范围。这就是使容器实现独一无二的原因。一些容器根本没有配置(它们依赖于自动装载),另一些依赖于通过回调定义的 PHP 代码,还有一些依赖于配置文件……这个标准只关注如何获取对象。
此外,用于对象的命名约定也不属于本 PSR 的范围。实际上,当您查看命名约定时,有两种策略:
- 实体标识符是类名或接口名(主要由具有自动装载功能的框架使用)
- 实体标识符是一个普通名称(更接近于变量名),主要由依赖于配置的框架使用。
两种策略都有其优点和缺点。此 PSR 的目标不是选择一种约定而不是另一种约定。相反,用户可以简单地使用别名来弥合具有不同命名策略的 2 个容器之间的差距。
- 推荐用法:容器 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你不应该注入容器,因为:
- 它使代码的互操作性降低:通过注入容器,您必须使用与 Container PSR 兼容的容器。使用另一个选项,您的代码可以与任何容器一起使用。
- 您正在强迫开发人员将其实体标识符命名为“db”。此命名可能与对另一个服务具有相同期望的另一个包冲突。
- 更难测试。
- 从您的代码中并不能直接清楚地表明
BadExample类依赖“db”服务。依赖项是隐藏的。
很多时候,ContainerInterface会被其他包使用。作为使用框架的最终用户 PHP 开发人员,您不太可能需要直接使用 ContainerInterface 的接口或类型提示。
在您的代码中使用容器 PSR 是否被认为合理,归结于您正在检索的对象是否是引用容器的对象的依赖项。这里还有几个例子:
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'));
}
}
- 历史
在将 Container PSR 提交给 PHP-FIG 之前,ContainerInterface首先是在一个名为 container-interop 的项目中提出的。该项目的目标是为实现 ContainerInterface 提供一个测试平台,并为 Container PSR 铺平道路。
- 接口名称
接口名称与 container-interop 中讨论的接口名称相同(仅更改命名空间以匹配其他 PSR)。它已在container-interop 上进行了深入讨论,并通过投票决定。
以下时各自的投票考虑的选项列表:
ContainerInterface: +8ProviderInterface: +2LocatorInterface: 0ReadableContainerInterface: -5ServiceLocatorInterface: -6ObjectFactory: -6ObjectStore: -8ConsumerInterface: -9
- 接口方法
接口将包含哪些方法的选择是在对现有容器进行统计分析之后做出的。
分析总结表明:
- 所有容器都提供了一种通过 id 获取对象的方法
- 绝大多数人将这种方法命名为
get() - 对于所有容器,
get()方法有 1 个字符串类型的强制参数 - 有些容器
get()方法有一个可选的参数,但它在容器之间没有相同的用途 - 大多数容器都提供了一种方法来测试它是否可以通过 id 返回对象
- 大多数使用的方法名称为
has() - 对于所有提供了
has()方法的容器,它们都有一个字符串参数 - 大多数容器在
get()方法没有查到对象时抛出异常,而不是返回 null - 绝大多数容器没有实现
ArrayAccess
容器中是否需要提供方法来定义对象,在 container-interop 项目开始时已经被讨论过了。讨论的结果是接口不需要提供这个方法,因为它超出了其范围(请参阅“目标”部分)。
因此,ContainerInterface包含两种方法:
get(),返回任何内容,带有一个强制字符串参数。如果找不到对象,则应抛出异常。has(),返回一个布尔值,带有一个强制字符串参数。
**get()**方法中的参数数量
虽然在ContainerInterface中的 get() 方法只定义了一个强制参数,但它与具有可选参数的现有容器并不兼容。PHP 允许实现提供更多参数,只要它们是可选的,因为实现类这样是符合接口要求的。
与容器互操作的区别:容器互操作规范指出:
虽然在
ContainerInterface中的get()方法只定义了一个强制参数,但实现可以接受额外的可选参数。
这句话从 PSR-11 中删除,因为:
- 它源于 PHP 中的 OOP 原则,因此与 PSR-11 没有直接关系
- 我们不想鼓励实现者添加额外的参数,因为我们建议面向接口而不是面向实现进行编码
然而,一些实现有额外的可选参数;这在技术上是合法的。这样的实现与 PSR-11 兼容。**$id**参数类型get() 和 has() 中的$id参数类型已经在容器互操作项目中讨论过。
虽然所有分析的容器中 $id 参数都是 string 类型,但是建议允许它可以是任何类型(比如对象),这样将允许容器提供更多高级的查询 API。
例如使用容器来作为对象构造器,$id 参数是对象就可以告诉容器怎么去创建一个对象实例。
讨论的结果是这超出了 $id 是用来从容器获取对象的范围, $id 是不知道对象是怎么创建的。对象参数更适合工厂类。
抛出异常
此 PSR 提供了 2 个接口,旨在由容器异常实现。
基础异常Psr\Container\ContainerExceptionInterface基本接口。它应该由容器直接抛出的自定义异常来实现。
作为容器一部分的任何异常都会实现ContainerExceptionInterface.。几个例子:
- 如果容器依赖于配置文件并且该配置文件存在缺陷,则容器可能会抛出一个
InvalidFileException实现ContainerExceptionInterface。 - 如果在依赖项之间检测到循环依赖项,容器可能会抛出一个
CyclicDependencyException实现ContainerExceptionInterface。
但是,如果异常是由容器范围之外的某些代码引发的(例如,在实例化对象时引发异常),则容器不需要实现ContainerExceptionInterface。
基本异常接口的作用受到质疑:它不是一个通常会捕获的异常。
然而,大多数 PHP-FIG 成员认为这是最佳实践。基本异常接口在以前的 PSR 和几个成员项目中实现。因此保留了基本异常接口。
未发现异常
使用不存在的 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接口的版本。
Implementors
- Acclimate
- Aura.DI
- dcp-di
- League Container
- Mouf
- Njasm Container
- PHP-DI
- PimpleInterop
- XStatic
- Zend ServiceManager
Middleware
Consumers
此列表并不全面,仅作为一个示例,表明对 PSR 有相当大的兴趣。
PSR-13:超媒体链接
在 HTML 上下文和各种 API 格式上下文中,超媒体链接正在成为 Web 中越来越重要的部分。然而,没有一种通用的超媒体格式,也没有一种通用的方式来表示格式之间的联系。
本规范旨在为 PHP 开发人员提供一种简单、通用的方式来表示超媒体链接,而与所使用的序列化格式无关。这反过来又允许系统将带有超媒体链接的响应序列化为一种或多种有线格式,而与决定这些链接应该是什么的过程无关。
- 规范
基本链接
超媒体链接至少包括:
- 表示被引用的目标资源的 URI。
- 定义目标资源如何与源相关的关系。
根据使用的格式,可能存在链接的各种其他属性。由于附加属性没有很好地标准化或通用,因此本规范不寻求对其进行标准化。
为本说明书的目的,以下定义适用。
- Implementing Object - 实现本规范定义的接口之一的对象。
- Serializer - 一个库或其他系统,它采用一个或多个 Link 对象并以某种定义的格式生成它的序列化表示。
链接属性
除了 URI 和关系之外,所有链接都可以包含零个或多个附加属性。这里没有允许的值的正式注册表,值的有效性取决于上下文,并且通常取决于特定的序列化格式。通常支持的值包括“hreflang”、“title”和“type”。
如果序列化格式要求,序列化程序可以省略链接对象的属性。然而,序列化程序应该编码所有可能提供的属性,以允许用户扩展,除非序列化格式的定义阻止。
某些属性(通常hreflang)可能在其上下文中出现多次。因此,一个属性值可能是一个值数组而不是一个简单的值。序列化程序可以以适合序列化格式的任何格式对数组进行编码(例如空格分隔的列表、逗号分隔的列表等)。如果一个给定的属性在特定的上下文中不允许有多个值,序列化程序必须使用提供的第一个值并忽略所有后续值。
如果属性值为 boolean true,则序列化程序可以在适当的情况下使用缩写形式并受序列化格式支持。例如,当属性的存在具有布尔含义时,HTML 允许属性没有值。当且仅当属性为 boolean 时,此规则才适用true,而不适用于 PHP 中的任何其他“真实”值,例如整数 1。
如果属性值为 boolean false,序列化程序应该完全省略该属性,除非这样做会改变结果的语义。当且仅当属性为 boolean 时,此规则才适用false,而不适用于 PHP 中的任何其他“假”值,例如整数 0。
链接关系
链接关系被定义为字符串,并且在公开定义的关系的情况下是简单的关键字,在私有关系的情况下是绝对的 URI。
如果使用简单的关键字,它应该匹配来自 IANA 注册中心的关键字:
http://www.iana.org/assignments/link-relations/link-relations.xhtml
可以选择使用 microformats.org 注册表,但这可能并非在每种情况下都有效:
http://microformats.org/wiki/existing-rel-values
未在上述注册表或类似公共注册表中定义的关系被视为“私有”,即特定于特定应用程序或用例。这种关系必须使用绝对 URI。
链接模板
RFC 6570 定义了 URI 模板的格式,即预期将使用客户端工具提供的值填充的 URI 模式。一些超媒体格式支持模板链接,而另一些则不支持,并且可能有一种特殊的方式来表示链接是模板。不支持 URI 模板的格式的序列化程序必须忽略它遇到的任何模板化链接。
链接提供者
在某些情况下,链接提供者可能需要添加额外链接的能力。在其他情况下,链接提供程序必须是只读的,链接在运行时从其他一些数据源派生。出于这个原因,可修改的提供者是可以选择实现的辅助接口。
此外,某些 Link Provider 对象,例如 PSR-7 Response 对象,在设计上是不可变的。这意味着在原地添加链接的方法将是不兼容的。因此,EvolvableLinkProviderInterface的单一方法要求返回一个新对象,与原始对象相同,但包含一个附加的 Link 对象。
链接对象
链接对象在大多数情况下是值对象。因此,允许它们以与 PSR-7 值对象相同的方式发展是一个有用的选择。因此,包含了一个额外的 EvolvableLinkInterface,它提供了通过一次更改生成新对象实例的方法。PSR-7 使用相同的模型,并且由于 PHP 的写时复制行为,它仍然具有 CPU 和内存效率。
但是,模板值没有可演化的方法,因为链接的模板值完全基于 href 值。它不能独立设置,而是从 href 值是否为 RFC 6570 链接模板派生而来。
- 包
接口和类的描述作为 psr/link 包的一部分提供。
- 接口
**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();
}
**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);
}
**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);
}
**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:超媒体链接 - 说明文档
- 概述
在 HTML 上下文和各种 API 格式上下文中,超媒体链接正在成为 Web 中越来越重要的部分。然而,没有单一的通用超媒体格式,也没有一种通用的方式来表示格式之间的链接。
本规范旨在为 PHP 开发人员提供一种简单、通用的方式来表示超媒体链接,而与所使用的序列化格式无关。这反过来又允许系统将带有超媒体链接的响应序列化为一种或多种有线格式,而与决定这些链接应该是什么的过程无关。
- 范围
目标
- 该规范旨在提取和标准化不同格式之间的超媒体链接表示。
非目标
- 本规范不寻求标准化或支持任何特定的超媒体序列化格式。
- 设计决策
为什么没有mutator方法?
本规范的主要目标之一是 PSR-7 响应对象。设计的响应对象必须是不可变的。其他值对象实现可能也需要不可变接口。
此外,某些 Link Provider 对象可能不是值对象,而是给定域中的其他对象,它们能够动态生成链接,可能来自数据库结果或其他底层表示。在这些情况下,可写提供程序定义将不兼容。
因此,本规范将访问器方法和可演化方法拆分为单独的接口,允许对象仅实现适合其用例的只读或可演化版本。
为什么链接对象上的 rel 是多值的?
不同的超媒体标准以不同的方式处理具有相同关系的多个链接。有些有一个定义了多个 rel 的链接。其他人只有一个 rel 对象,然后包含多个链接。
唯一地定义每个链接但允许它具有多个 rel 提供了最兼容的分母定义。一个单独的 LinkInterface 对象可以在适当的时候以给定的超媒体格式序列化为一个或多个链接对象。但是,指定多个链接对象,每个对象都有一个 rel 但相同的 URI 也是合法的,超媒体格式也可以适当地序列化它。
为什么需要 LinkProviderInterface?
在许多情况下,一组链接将附加到某个其他对象。这些对象可以用于所有相关的情况是它们的链接,或者它们的链接的某个子集。例如,可以定义各种不同的值对象来表示不同的 REST 格式,例如 HAL、JSON-LD 或 Atom。从这样的对象中统一提取这些链接以进行进一步处理可能很有用。例如,可以从对象中提取下一个/上一个链接,并将其作为链接头添加到 PSR-7 响应对象中。或者,许多链接用“预加载”链接关系表示是有意义的,这将向 HTTP 2 兼容的 Web 服务器指示链接的资源应该流式传输到客户端以预期后续请求。
所有这些情况都与对象的有效负载或编码无关。通过提供访问此类链接的通用接口,我们可以对链接本身进行通用处理,而不管生成它们的值对象或域对象如何。
PSR-14:事件调度
事件分发是一种常见且经过充分测试的机制,允许开发人员轻松一致地将逻辑注入应用程序。
该 PSR 的目标是为基于事件的扩展和协作建立一个通用机制,以便库和组件可以在各种应用程序和框架之间更自由地重用。
- 目标
拥有用于调度和处理事件的通用接口允许开发人员创建可以以通用方式与许多框架和其他库交互的库。
一些例子:
- 当用户没有权限时,将阻止保存/访问数据的安全框架。
- 一个常见的整页缓存系统。
- 扩展其他库的库,无论它们都集成到什么框架中。
- 一个日志包,用于跟踪应用程序中采取的所有操作。
- 定义
- Event - Event 是由 Emitter 产生的消息。它可以是任意 PHP 对象。
- 侦听器 - 侦听器是期望传递事件的任何 PHP 可调用对象。零个或多个侦听器可以传递相同的事件。如果侦听器愿意,它可以将其他一些异步行为排入队列。
- 发射器 - 发射器是任何希望调度事件的任意代码。这也称为“调用代码”。它不是由任何特定的数据结构表示的,而是指用例。
- Dispatcher - Dispatcher 是一个服务对象,由 Emitter 赋予 Event 对象。Dispatcher 负责确保将 Event 传递给所有相关的 Listener,但必须将确定负责的 listener 推迟到 Listener Provider。
- 侦听器提供者 - 侦听器提供者负责确定哪些侦听器与给定事件相关,但不得调用侦听器本身。侦听器提供者可以指定零个或多个相关侦听器。
事件
事件是充当发射器和适当侦听器之间通信单元的对象。
如果用例调用侦听器向发射器提供信息,则事件对象可能是可变的。但是,如果不需要这种双向通信,则建议将事件定义为不可变的;即,定义为它缺少 mutator 方法。
实现者必须假设同一个对象将被传递给所有侦听器。
建议但不要求 Event 对象支持无损序列化和反序列化;$event == unserialize(serialize($event))应该成立。如果合适,事件对象可以利用 PHP 的 Serializable 接口,__sleep() 或 __wakeup() 魔术方法或类似的语言的功能。
可终止事件
可终止事件是事件的一种特殊情况,它包含防止调用更多侦听器的其他方法。通过实现StoppableEventInterface。
实现的事件StoppableEventInterface必须从isPropagationStopped()所代表的任何事件完成时返回 true。由类的实现者来自行决定。例如,请求 PSR-7RequestInterface对象与相应ResponseInterface对象匹配的事件可能具有供侦听器调用的 setResponse(ResponseInterface $res) 方法,这会导致isPropagationStopped()返回true。
侦听器
侦听器可以是任何 PHP 可调用的。一个侦听器必须有一个且只有一个参数,即它响应的事件。侦听器应该键入与其用例相关的具体参数;也就是说,一个侦听器可以对接口进行类型提示,以表明它与实现该接口的任何事件类型兼容,或者与该接口的特定实现兼容。
一个侦听器应该有一个void返回,并且应该输入显式返回的提示。Dispatcher 必须忽略来自 Listeners 的返回值。
侦听器可以将操作委托给其他代码。这包括一个侦听器,它是一个围绕运行实际业务逻辑的对象的瘦包装器。
侦听器可以使用 cron、队列服务器或类似技术将事件中的信息排入队列以供辅助进程稍后处理。它可以序列化 Event 对象本身来这样做;但是,应注意并非所有 Event 对象都可以安全地序列化。辅助进程必须假定它对事件对象所做的任何更改都不会传播到其他侦听器。
调度
Dispatcher 是一个实现EventDispatcherInterface。它负责从侦听器提供程序中检索已调度事件的侦听器,并使用该事件调用每个侦听器。
调度员:
- 必须按照从 ListenerProvider 返回的顺序同步调用 Listener。
- 在完成调用侦听器后,必须返回它传递的相同事件对象。
- 在所有侦听器都执行之前,不得返回到发射器。
如果传递了可终止事件,则调度程序
- 必须在调用每个侦听器之前调用事件
isPropagationStopped()。如果该方法返回true,它必须立即将事件返回给发射器,并且不得调用任何进一步的侦听器。这就意味着如果传递给分发器的事件在调用 isPropagationStopped() 后总是true,将不会有侦听器被调用。
Dispatcher 应该假设从 Listener Provider 返回给它的任何 Listener 都是类型安全的。也就是说,Dispatcher 应该假设调用$listener($event)不会产生TypeError。
错误处理
侦听器抛出的异常或错误必须阻止任何其他侦听器的执行。必须允许侦听器抛出的异常或错误传播回发射器。
Dispatcher 可以捕获一个抛出的对象来记录它,允许采取额外的行动,等等,但必须重新抛出原始的 throwable。
侦听器提供者
侦听器提供者是一个服务对象,负责确定与给定事件相关的侦听器和应该调用的侦听器。它可以确定哪些侦听器是相关的,以及通过它选择的任何方式返回它们的顺序。这可能包括:
- 允许某种形式的注册机制,以便实现者可以按固定顺序将侦听器分发给事件。
- 根据 Event 的类型和实现的接口,通过反射推导出适用的 Listeners 列表。
- 提前生成可以在运行时查询的侦听器的编译列表。
- 实现某种形式的访问控制,以便仅当当前用户具有特定权限时才会调用某些侦听器。
- 从事件引用的对象(例如实体)中提取一些信息,并在该对象上调用预定义的生命周期方法。
- 使用一些任意逻辑将其职责委托给一个或多个其他侦听器提供程序。
可以根据需要使用上述任何组合或其他机制。
侦听器提供者应该使用事件的类名来区分一个事件和另一个事件。他们还可以酌情考虑有关该事件的任何其他信息。
在确定侦听器适用性时,侦听器提供者必须将父类型与事件自己的类型相同地对待。在以下情况下:
class A {}
class B extends A {}
$b = new B();
function listener(A $event): void {};
侦听器提供者必须将listener()其视为适用的侦听器$b,因为它是类型兼容的,除非某些其他标准阻止它这样做。
对象组合
一个 Dispatcher 应该组成一个 Listener Provider 来确定相关的侦听器。建议将 Listener Provider 实现为与 Dispatcher 不同的对象,但这不是必需的。
- 接口 ```php namespace Psr\EventDispatcher;
/**
- 定义事件的调度程序
/
interface EventDispatcherInterface
{
/*
- 为所有相关侦听器提供一个要处理的事件 *
- @param object $event
- 要处理的对象 *
- @return object
- 传递的事件
*/
public function dispatch(object $event);
}
php namespace Psr\EventDispatcher;
/**
- 将事件映射到适用于该事件的侦听器
/
interface ListenerProviderInterface
{
/*
- @param object $event
- 相关侦听器的事件
- @return iterable
- 可调用对象的可迭代对象(数组、迭代器或生成器)。每个可调用对象必须与$event类型兼容
*/
public function getListenersForEvent(object $event) : iterable;
}
php namespace Psr\EventDispatcher;
/**
- 当事件被处理时,其处理可能被中断的事件 *
- Dispatcher实现必须检查以确定在调用每个侦听器后是否将Event标记为停止,
- 如果是,那么它应该立即返回,而不调用任何进一步的Listeners。 / interface StoppableEventInterface { /*
- 概述
本文档的目的是描述 Event Dispatcher 规范背后的基本原理和逻辑。
- 为什么需要它?
许多库、组件和框架长期以来都支持允许任意第三方代码与其交互的机制。大多数是经典观察者模式的变体,通常通过中间对象或服务进行调解。其他人则采用更面向方面的编程 (AOP) 方法。尽管如此,它们都有相同的基本概念:在固定点中断程序流,以向任意第三方库提供有关正在执行的操作的信息,并允许它们做出反应或影响程序行为。
这是一个成熟的模型,但库这样做的标准机制将允许它们与越来越多的第三方库进行互操作,而原始开发人员和扩展开发人员的工作量更少。
- 范围
目标
- 简化和标准化库和组件可以通过“事件”将自己暴露给扩展的过程,以便它们可以更容易地合并到应用程序和框架中。
- 简化和标准化库和组件可能注册对事件响应感兴趣的过程,以便它们可以更容易地合并到任意应用程序和框架中。
- 在可行的范围内,简化现有代码库向本规范过渡的过程。
非目标
- 异步系统通常有一个“事件循环”的概念来管理交错协同程序。这是不相关的事情,并且与本规范明确无关。
- 实现“事件源”模式的存储系统也有“事件”的概念。这与此处讨论的事件无关,并且明确超出范围。
- 与现有事件系统的严格向后兼容性不是优先事项,也不是预期的。
- 尽管本规范无疑会建议实现模式,但它并不寻求定义一个真正的事件调度器实现,而只是定义调用者和侦听器如何与该调度器进行通信。
- 方案
考虑的用例
工作组根据各种系统中常见的用例确定了四种可能的事件传递工作流程。
- 单向通知。(“我做了一件事,如果你在乎的话。”)
- 对象增强。(“这是一个东西,请在我处理它之前修改它。”)
- 收藏。(“把你所有的东西都给我,我可以用那个清单做点什么。”)
- 替代链。(“这是一件事;你们中的第一个可以处理它的人这样做,然后停下来。”)
经过进一步审查,工作组确定:
- 集合是对象增强的一个特例(集合是被增强的对象)。
- 替代链同样是对象增强的一种特殊情况,因为签名是相同的,并且调度工作流几乎是相同的,尽管包括了额外的检查。
- 单向通知是其他通知的退化情况,或者可以这样表示。
虽然在概念上单向通知可以异步完成(包括通过队列延迟它),但实际上,该模型的显式实现很少存在,提供的细节指导的地方更少(例如正确的错误处理)。经过深思熟虑,工作组选择不为单向通知提供一个明确独立的工作流程,因为它可以充分地表示为其他方面的退化案例。
示例应用
- 指示系统配置或用户操作发生了某些更改,并允许其他系统以不影响程序流程的方式做出反应(例如发送电子邮件或记录操作)。
- 将对象传递给一系列侦听器以允许在将其保存到持久性系统之前对其进行修改。
- 将集合传递给一系列侦听器以允许它们向其注册值或修改现有值,以便发射器可以对所有收集的信息采取行动。
- 将一些上下文信息传递给一系列侦听器,以便所有侦听器都可以“投票”决定采取什么行动,而发射器则根据提供的聚合信息做出决定。
- 将对象传递给一系列侦听器,并允许任何侦听器在其他侦听器完成之前提前终止进程。
不可变事件
最初,工作组希望将所有事件定义为不可变消息对象,类似于 PSR-7。然而,除了单向通知案例之外,这在所有情况下都被证明是有问题的。在其他场景中,侦听器需要一种将数据返回给调用者的方法。在概念上,有三种可能的途径:
- 使事件可变并就地修改它。
- 要求事件是可进化的(不可变的,但使用 PSR-7 和 PSR-13 等
with*()方法)并且监听器返回事件以传递。 - 使 Event 不可变,但聚合并返回每个 Listener 的返回值。
但是,可停止事件(替代链情况)也需要有一个通道来指示不应调用更多的侦听器。这可以通过以下方式完成:
- 修改事件(例如,调用
stopPropagation()方法) - 从侦听器返回一个标记值(
true或false)以指示传播应该终止。 - 演化要停止的事件 (
withPropagationStopped())
这些替代方案中的每一个都有缺点。第一个意味着,至少为了指示传递状态,事件必须是可变的。第二个要求侦听器返回一个值,至少在他们打算停止事件传播时;这可能会对现有的产生影响,并可能在文档方面出现问题。第三个要求 Listeners 在所有情况下都返回 Event 或 mutated Event,并要求 Dispatchers 进行测试以确保返回的值与传递给 Listener 的值的类型相同;它有效地使消费者和实施者都承担了责任,从而引发了更多潜在的集成问题。
此外,一个理想的功能是能够根据从侦听器收集的值推导出是否停止传递。(例如,在其中一个提供了某个值时停止,或者在其中至少三个指示“拒绝此请求”标志之后停止,或类似的。)虽然在技术上可以实现为可进化的对象,但这种行为本质上是有状态的,因此对于实现者和用户来说都是非常麻烦的。
让侦听器返回可进化事件也带来了挑战。PHP 或其他地方的任何已知实现都不使用该模式。它还依赖于侦听器记住返回事件(侦听器本身的额外工作)并且不返回可能与后来的侦听器不完全兼容的其他一些新对象(例如事件的子类或超类)。
不可变事件也依赖于事件本身来尊重不可变的警告。事件本质上是非常松散的设计,实现者忽略规范的这一部分的可能性很高,即使是无意的,也是很高的。
这留下了两个可能的选择:
- 允许事件是可变的。
- 需要但不能强制执行具有高级接口的不可变事件,侦听器本身需要做更多的工作,并且在编译时可能无法检测到更高的破坏可能性。
通过“隆重的仪式”,我们暗示需要冗长的语法或实现。在前一种情况下,Listener 本身需要 (a) 创建一个新的 Event 实例并切换传递标志,并 (b) 返回新的 Event 实例,以便 Dispatcher 可以检查它:
function (SomeEvent $event) : SomeEvent
{
// do some work
return $event->withPropagationStopped();
}
后一种情况,Dispatcher 实现,需要检查返回值:
foreach ($provider->getListenersForEvent($event) as $listener) {
$returnedEvent = $listener($event);
if (! $returnedEvent instanceof $event) {
// This is an exceptional case!
//
// We now have an event of a different type, or perhaps nothing was
// returned by the listener. An event of a different type might mean:
//
// - we need to trigger the new event
// - we have an event mismatch, and should raise an exception
// - we should attempt to trigger the remaining listeners anyway
//
// In the case of nothing being returned, this could mean any of:
//
// - we should continue triggering, using the original event
// - we should stop triggering, and treat this as a request to
// stop propagation
// - we should raise an exception, because the listener did not
// return what was expected
//
// In short, this becomes very hard to specify, or enforce.
}
if ($returnedEvent instanceof StoppableEventInterface
&& $returnedEvent->isPropagationStopped()
) {
break;
}
}
在这两种情况下,我们都会引入更多潜在的边缘案例,但几乎没有什么好处,并且很少有语言级别的机制来指导开发人员正确实现。
鉴于这些选项,工作组认为可变事件是更安全的选择。
也就是说,不要求 Event 是 mutable。当且仅当有必要且适合手头的用例时,实施者才应在 Event 对象上提供增变器方法。
监听注册
在规范开发过程中的实验确定了有多种可行的、合法的方式可以通知调度程序有关侦听程序的信息:
- 可以显式注册;
- 可以基于其签名的反映显式注册;
- 可以用数字优先顺序注册;
- 可以使用前/后机制进行注册,以更精确地控制排序;
- 可以从服务容器注册;
- 可以使用预编译步骤生成代码;
- 可以基于事件本身中对象的方法名称;
- 可以基于任意复杂的逻辑限制在某些情况或上下文中(仅适用于某些用户,仅在某些日子,仅当某些系统设置存在时,等等)。
这些和其他机制今天都在 PHP 中广泛存在,都是值得支持的有效用例,而且很少有(如果有的话)可以方便地表示为另一种特殊情况。也就是说,在不切断许多应支持的用例的情况下,标准化一种方式,甚至是一小部分方式来通知系统侦听器,即使不是不可能的,也是不切实际的。
因此工作组选择将 Listeners 的注册封装在 ListenerProviderInterface。 一个 Provider 对象可能有一个可用的显式注册机制,或者多个这样的机制,或者没有。它也可以由某些编译步骤生成的代码生成。但是,这也将管理分发事件的过程与将事件映射到侦听器的过程的责任分开。这样,不同的实现可以根据需要与不同的 Provider 机制混合匹配。
甚至有可能,并且可能是可取的,允许库包含他们自己的提供者,这些提供者被聚合到一个公共提供者中,该提供者聚合他们的侦听器以返回到调度程序。这是在任意框架内处理任意侦听器注册的一种可能方式,尽管工作组很清楚这不是唯一的选择。
虽然将 Dispatcher 和 Provider 组合成一个对象是一种有效且允许的退化情况,但不建议这样做,因为它会降低系统集成商的灵活性。相反,提供者应该被组合成一个依赖对象。
延迟监听
规范要求在 Dispatcher 返回之前必须全部调用 Provider 返回的可调用对象(除非显式停止传递)。但是,规范还明确指出,侦听器可以将事件排入队列以供以后处理,而不是立即采取行动。提供者也完全允许接受可调用对象的注册,但随后将其包装在另一个可调用对象中,然后再将其返回给调度程序。(在这种情况下,从 Dispatcher 的角度来看,包装器是 Listener。)这使得以下所有行为都是合法的:
- 提供者返回提供给他们的可调用侦听器。
- 提供者返回可调用对象,这些可调用对象在队列中创建一个对象,该对象将在稍后的某个时间点用另一个可调用对象对事件做出反应。
- 侦听器自己可能会在队列中创建一个对象,该对象将在稍后的某个时间点对事件做出反应。
- 如果在支持异步行为的环境中运行,侦听器或提供者可能会触发异步任务(假设发射器不需要异步任务的结果。)
- 提供者可以基于任意逻辑有选择地对侦听器执行此类延迟或封装。
最终结果是提供者和侦听器负责确定何时可以安全地将对事件的响应推迟到稍后的时间。在这种情况下,Provider 或 Listener 明确选择不将有意义的数据传回 Emitter,但工作组确定他们处于最佳位置,可以知道这样做是否安全。
虽然从技术上讲是设计的副作用,但它本质上与 Laravel(从 Laravel 5 开始)使用的方法相同,并且已得到证明。
返回值
根据规范,Dispatcher 必须返回 Emitter 传递的事件。这旨在为用户提供更符合人体工程学的体验,允许类似于以下的短手:
$event = $dispatcher->dispatch(new SomeEvent('some context'));
$items = $dispatcher->dispatch(new ItemCollector())->getItems();
但是,EventDispatcher::dispatch()接口没有指定返回类型。这主要是为了与现有实现向后兼容,以使它们更容易采用新接口。此外,由于事件可以是任何返回类型,这将只提供最小(尽管非零)值,因为该类型声明不会为 IDE 提供任何有用的信息,也不会有效地强制执行相同的返回事件。因此,方法返回在语法上是无类型的。但是,返回相同的 Event 对象dispatch()仍然是一项要求,如果不这样做是违反规范的。
PSR-16:简单缓存
本文档描述了一个用于缓存项和缓存驱动程序的简单且易可扩展的接口。
最终的实现可能比提议有更多的功能,但它们必须首先实现指定的接口/功能。
- 概述
缓存是提高任何项目性能的常用方法,使缓存库成为许多框架和库的最常见功能之一。此级别的互操作性意味着库可以放弃自己的缓存实现,并轻松依赖框架提供的缓存实现,或另一个专用缓存库。
PSR-6 已经解决了这个问题,但是以一种相当正式和冗长的方式来满足最简单的用例所需要的。这种更简单的方法旨在为常见案例构建标准化的流线型界面。它独立于 PSR-6,但旨在使与 PSR-6 的兼容性尽可能简单。
- 定义
Calling Library、Implementing Library、TTL、Expiration 和 Key 的定义从 PSR-6 复制而来,因为相同的假设是正确的。
- 调用库 - 实际需要缓存服务的库或代码。该库实现了标准接口的缓存服务,但不需要知道这些缓存服务的具体实现。
- 实施库 - 该库负责实现此标准,以便为任何调用库提供缓存服务。实现库必须提供一个实现 Psr\SimpleCache\CacheInterface 接口的类。实现库必须至少支持 TTL 功能。
- TTL - 缓存项的生存时间 (TTL) 是从存储该缓存项到它被认为是已过期的时间量。TTL 通常由表示时间的整数(以秒为单位)或 DateInterval 对象定义。
- Expiration - 缓存项设置为过时的实际时间。这是通过将 TTL 与存储对象的时间相加来计算的。在 1:30:00 存储 300 秒 TTL 的缓存项将在 1:35:00 到期。实施库可能会在其请求的过期时间之前使缓存项过期,但一旦达到其过期时间,必须将缓存项视为过期。如果调用库要求保存缓存项但未指定过期时间,或指定空过期时间或 TTL,则实施库可以使用配置的默认持续时间。如果没有设置默认持续时间,实现库必须将其解释为永久缓存项的请求,或者只要底层实现支持。如果提供了负数或零 TTL,则必须从缓存中删除该缓存项(如果它存在),因为它已经过期。
- Key - 一个至少包含一个字符的字符串,用于唯一标识缓存项。实现库必须支持由字符
A-Z、a-z、0-9、_和.UTF-8 编码的任意顺序组成的键,并且长度最多为 64 个字符。实现库可以支持额外的字符和编码或更长的长度,但必须至少支持该最小值。库负责自己对密钥字符串进行适当的转义,但必须能够返回原始未修改的密钥字符串。以下字符为将来的扩展保留,实现库不得支持:{}()/\@: - 缓存 - 实现
Psr\SimpleCache\CacheInterface接口的对象。 - 缓存未命中 - 缓存未命中将返回 null,因此检测是否
null无法存储。这是与 PSR-6 假设的主要偏差。
如果未为特定缓存项指定默认 TTL,则实现可以为用户提供一种机制来指定默认 TTL。如果没有提供用户指定的默认值,实现必须默认为底层实现允许的最大合法值。如果底层实现不支持 TTL,用户指定的 TTL 必须被忽略。
- 数据
实现库必须支持所有可序列化的 PHP 数据类型,包括:
- 字符串 - 任何 PHP 兼容编码中任意大小的字符串。
- 整数 - PHP 支持的任何大小的所有整数,最多 64 位有符号。
- 浮点数 - 所有带符号的浮点值。
- 布尔值 - 真假。
- Null - 空值(尽管在读回时无法将其与缓存未命中区分开来)。
- 数组 - 任意深度的索引、关联和多维数组。
- 对象 - 任何支持无损序列化和反序列化的对象,例如
$o == unserialize(serialize($o))。对象可以利用 PHP 的 Serializable 接口,__sleep()和__wakeup()魔术方法或类似的语言功能(如果合适)。
传递到实现库的所有数据必须完全按照传递的方式返回。这包括变量类型。也就是说,如果 (int) 5 是保存的值,则返回 (string) 5 是错误的。实现库可以在内部使用 PHP 的 serialize()/unserialize() 函数,但不是必须这样做。与它们的兼容性只是用作可接受对象值的基线。
如果由于任何原因无法返回准确的保存值,实现库必须响应缓存未命中而不是损坏的数据。
- 接口
缓存接口定义了对缓存对象集合的最基本操作,这需要基本的读取、写入和删除单个缓存项。
此外,它还具有处理多组缓存对象的方法,例如一次写入、读取或删除多个缓存对象。当您要执行大量缓存读取/写入时,这很有用,并且允许您在对缓存服务器的一次调用中执行您的操作,从而显著减少延迟时间。
CacheInterface 的实例对应于具有单个键命名空间的单个缓存项集合,相当于 PSR-6 中的“池”。不同的 CacheInterface 实例可以由同一个数据存储支持,但必须在逻辑上独立。
<?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 成功返回true.失败返回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,有任何错误时返回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);
}
CacheException
<?php
namespace Psr\SimpleCache;
/**
* 库抛出异常的接口,用于所有类型异常
*/
interface CacheException
{
}
InvalidArgumentException
<?php
namespace Psr\SimpleCache;
/**
* 无效缓存参数异常的接口
*
* 当传递一个无效参数时,必须抛出一个实现了此接口的异常
*/
interface InvalidArgumentException extends CacheException
{
}
PSR-16:简单缓存 - 说明文档
- 概述
缓存是提高任何项目性能的常用方法,许多库都使用或可以使用它。此级别的互操作性意味着库可以放弃自己的缓存实现,并轻松依赖框架提供的缓存实现,或用户选择的另一个专用缓存库。
- 为什么需要它?
PSR-6 已经解决了这个问题,但是以一种相当正式和冗长的方式来满足最简单的用例所需要的。这种更简单的方法旨在在现有 PSR-6 接口之上构建一个标准化的简单层。
- 范围
目标
- 一个简单的缓存操作接口。
- 出于性能(往返时间)原因,对多个键的操作的基本支持。
- 提供将 PSR-6 实现转换为 PSR-Simple-Cache 的适配器类。
- 很有可能将所有的缓存 PSR 从缓存库公开。
非目标
- PSR-6 解决了所有可能的极端情况,已经做得很好。
- 方案
这里选择的方法在设计上是非常简单的,因为它只用于最简单的情况。它不必由所有可能的缓存后端实现,也不必用于所有用途。它只是 PSR-6 之上的一层封装。
HTTP
可互操作的标准和接口,在客户端和服务端有一种不可知的方法来处理 HTTP 请求和响应。
PSR-7:HTTP 消息接口
本文档描述了 RFC 7230 和 RFC 7231 中描述的用于表示 HTTP 消息的通用接口,以及 RFC 3986 中描述的用于 HTTP 消息的 URI 。
HTTP 消息是 Web 开发的基础。Web 浏览器和 HTTP 客户端(例如 cURL)创建 HTTP 请求消息,这些消息发送到 Web 服务器,服务器提供 HTTP 响应消息。服务器端代码接收 HTTP 请求消息,并返回 HTTP 响应消息。
HTTP 消息通常是从最终用户消费者中抽象出来的,但作为开发人员,我们通常需要知道它们的结构以及如何访问或操作它们以执行我们的任务,无论这是否可能向 HTTP API 发出请求,或处理传入的请求。
每个 HTTP 请求消息都有一个特定的形式:
POST /path HTTP/1.1
Host: example.com
foo=bar&baz=bat
请求的第一行是“请求行”,依次包含 HTTP 请求方法、请求目标(通常是绝对 URI 或 Web 服务器上的路径)和 HTTP 协议版本。其后是一个或多个 HTTP 标头、一个空行和消息正文。
HTTP 响应消息具有类似的结构:
HTTP/1.1 200 OK
Content-Type: text/plain
This is the response body
第一行是“状态行”,依次包含 HTTP 协议版本、HTTP 状态代码和“原因短语”,即状态代码的人类可读描述。与请求消息一样,这之后是一个或多个 HTTP 标头、一个空行和消息正文。
本文档中描述的接口是围绕 HTTP 消息和组成它们的元素的抽象。
- 规范
消息
HTTP 消息要么是从客户端到服务器的请求,要么是从服务器到客户端的响应。该规范分别定义了 HTTP 消息 Psr\Http\Message\RequestInterface的接口和Psr\Http\Message\ResponseInterface的接口。Psr\Http\Message\RequestInterface和Psr\Http\Message\ResponseInterface继承Psr\Http\Message\MessageInterface。虽然Psr\Http\Message\MessageInterface可以直接实现,但实现者应该实现 Psr\Http\Message\RequestInterface和 Psr\Http\Message\ResponseInterface。
从这里开始,Psr\Http\Message在引用这些接口时将省略命名空间。
HTTP 标头
不区分大小写的标题字段名称
HTTP 消息包含不区分大小写的标头字段名称。标题是按名称从 MessageInterface 以不区分大小写的方式实现的类中检索的。例如,检索 foo 标头将返回与检索标头相同的结果 FoO。同样,设置 Foo 标头将覆盖任何先前设置的 foo 标头值。
$message = $message->withHeader('foo', 'bar');
echo $message->getHeaderLine('foo');
// Outputs: bar
echo $message->getHeaderLine('FOO');
// Outputs: bar
$message = $message->withHeader('fOO', 'baz');
echo $message->getHeaderLine('foo');
// Outputs: baz
尽管可以不区分大小写地检索标头,但实现必须保留原始大小写,特别是在使用 getHeaders()检索时。
不符合标准的 HTTP 应用程序可能取决于特定的情况,因此用户在创建请求或响应时能够指示 HTTP 标头的大小写是很有用的。
具有多个值的标头
为了容纳具有多个值的标头,但仍提供将标头作为字符串处理的便利,可以从 MessageInterface 的实例中检索标头作为数组或字符串。使用 getHeaderLine()方法以字符串形式检索标头值,该字符串包含不区分大小写的标头的所有标头值,名称以逗号连接。用于getHeader()按名称检索特定不区分大小写的标头的所有标头值的数组。
$message = $message
->withHeader('foo', 'bar')
->withAddedHeader('foo', 'baz');
$header = $message->getHeaderLine('foo');
// $header contains: 'bar,baz'
$header = $message->getHeader('foo');
// ['bar', 'baz']
注意:并非所有标头值都可以使用逗号连接(例如, Set-Cookie)。使用此类标头时,基于 MessageInterface 的类的消费者应该依赖于getHeader()检索此类多值标头的方法。
主机头
在请求中,Host标头通常反映 URI 的主机组件,以及建立 TCP 连接时使用的主机。但是,HTTP 规范允许Host标头与两者不同。
在构建期间,如果没有提供标头,实现必须尝试Host从提供的 URI设置Host标头。
默认情况下,RequestInterface::withUri() 将返回请求的 Host标头替换Host为与传递的主机组件匹配的 UriInterface 标头。
你可以提供传递第二个参数为 true 来保证返回的消息实例中,原有的 host 头信息不会被替代掉。
以下表格说明了当 withUri() 的第二个参数被设置为 true 时,返回的消息实例中调用 getHeaderLine('Host') 方法会返回的内容:
| 请求主机头1 | 请求URI主机头2 | 传递URI主机头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 的主机头。
- 3 通过 withUri() 注入的 URI 的主机头。
Streams
HTTP 消息由起始行、标头和正文组成。HTTP 消息的正文可以非常小或非常大。尝试将消息正文表示为字符串很容易消耗比预期更多的内存,因为正文必须完全存储在内存中。尝试将请求或响应的主体存储在内存中会妨碍使用该实现来处理大型消息主体。StreamInterface用于在读取或写入数据流时隐藏实现细节。对于字符串将是适当的消息实现的情况,可以使用内置流,例如php://memory和 php://temp。StreamInterface公开了几种方法,可以有效地读取、写入和遍历流。
流使用三种方法公开其功能:isReadable()、 isWritable()和isSeekable()。流协作者可以使用这些方法来确定流是否能够满足他们的要求。
每个流实例都有不同的能力:它可以是只读的、只写的或读写的。它还可以允许任意随机访问(向前或向后寻找任何位置),或仅允许顺序访问(例如在套接字、管道或基于回调的流的情况下)。
最后,StreamInterface定义了一种__toString()方法来简化一次检索或输出整个正文内容。
与请求和响应接口不同,StreamInterface它不建模不变性。在包装实际 PHP 流的情况下,不可能强制执行不变性,因为与资源交互的任何代码都可能改变其状态(包括光标位置、内容等)。我们的建议是实现对服务器端请求和客户端响应使用只读流。消费者应该意识到流实例可能是可变的,因此可能会改变消息的状态;如有疑问,请创建一个新的流实例并将其附加到消息以强制执行状态。
请求目标和 URIs
根据 RFC 7230,请求消息包含“请求目标”作为请求行的第二段。请求目标可以是以下形式之一:
- origin-form,由路径和查询字符串(如果存在)组成;这通常称为相对 URL。通过 TCP 传输的消息通常是原始形式的;方案和权限数据通常仅通过 CGI 变量呈现。
- absolute-form,由方案、权限(“[user-info@]host[:port]”,其中括号中的项目是可选的)、路径(如果存在)、查询字符串(如果存在)和片段(如果存在)。这通常被称为绝对 URI,并且是唯一一种指定 URI 的形式,详见 RFC 3986。这种形式通常在向 HTTP 代理发出请求时使用。
- authority-form,仅包含权限。这通常仅用于 CONNECT 请求,以在 HTTP 客户端和代理服务器之间建立连接。
- asterisk-form,仅由字符串
*组成,与 OPTIONS 方法一起用于确定 Web 服务器的一般功能。
除了这些请求目标之外,通常还有一个与请求目标分开的“有效 URL”。有效 URL 不在 HTTP 消息中传输,但它用于确定发出请求的协议 (http/https)、端口和主机名。
有效 URL 由 UriInterface 表示。UriInterface为 RFC 3986(主要用例)中指定的 HTTP 和 HTTPS URI 建模。该接口提供了与各种 URI 部分交互的方法,这将避免重复解析 URI 的需要。它还定义了 __toString() 方法, 一种将建模的 URI 转换为其字符串表示的方法。
当使用 getRequestTarget() 检索请求目标时,默认情况下,此方法将使用 URI 对象并提取所有必要的组件来构造 origin-form。origin-form 是迄今为止最常见的 request-target 。
如果最终用户希望使用其他三种形式之一,或者如果用户想要显式覆盖请求目标,则可以使用withRequestTarget()。
调用此方法不会影响 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() 的返回值,默认情况下连接这两个值。
选择不实现 4 个请求目标表单中的一个或多个的客户端,必须仍然使用getRequestTarget()。这些客户端必须拒绝他们不支持的请求目标,并且不得依赖getUri()。RequestInterface提供用于检索请求目标或使用提供的请求目标创建新实例的方法。默认情况下,如果实例中没有专门组合 request-target,getRequestTarget() 将返回组合 URI 的 origin-form(如果没有组合 URI,则返回“/”)。 withRequestTarget($requestTarget)创建具有指定请求目标的新实例,从而允许开发人员创建代表其他三种请求目标形式(绝对形式、授权形式和星号形式)的请求消息。使用时,组合的 URI 实例仍然可以使用,特别是在客户端中,它可用于创建与服务器的连接。
服务器端请求RequestInterface提供 HTTP 请求消息的一般表示。但是,由于服务器端环境的性质,服务器端请求需要额外处理。服务器端处理需要考虑通用网关接口 (CGI),更具体地说,PHP 通过其服务器 API (SAPI) 对 CGI 的抽象和扩展。PHP 通过超全局变量简化了输入编组,例如:
$_COOKIE,它反序列化并提供对 HTTP cookie 的简化访问。$_GET,它反序列化并提供对查询字符串参数的简化访问。$_POST,它反序列化并提供对通过 HTTP POST 提交的 urlencoded 参数的简化访问;一般可以认为是解析消息体的结果。$_FILES,它提供有关文件上传的序列化元数据。$_SERVER,它提供对 CGI/SAPI 环境变量的访问,这些环境变量通常包括请求方法、请求方案、请求 URI 和标头。
ServerRequestInterface继承RequestInterface以提供围绕这些各种超全局变量的抽象。这种做法有助于减少消费者与超全局变量的耦合,并鼓励和促进测试请求消费者的能力。
服务器请求提供了一个附加属性“attributes”,以允许消费者根据应用程序特定的规则(例如路径匹配、方案匹配、主机匹配等)自检、分解和匹配请求。因此,服务器请求还可以在多个请求消费者之间提供消息传递。
上传文件ServerRequestInterface指定用于检索规范化结构中的上传文件树的方法,每个叶子都有一个 UploadedFileInterface。
在处理文件输入数组时,超全局$_FILES存在一些众所周知的问题。例如,如果您有一个提交文件数组的表单——例如,输入名称“files”,则提交files[0]和files[1]——PHP 将其表示为:
array(
'files' => array(
'name' => array(
0 => 'file0.txt',
1 => 'file1.html',
),
'type' => array(
0 => 'text/plain',
1 => 'text/html',
),
/* etc. */
),
)
而不是预期的:
array(
'files' => array(
0 => array(
'name' => 'file0.txt',
'type' => 'text/plain',
/* etc. */
),
1 => array(
'name' => 'file1.html',
'type' => 'text/html',
/* etc. */
),
),
)
结果是消费者需要了解这种语言实现细节,并编写代码来收集给定上传的数据。
此外,$_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 instance */
)
对于名称使用数组表示法的输入:
<input type="file" name="my-form[details][avatar]" />
$_FILES最终看起来像这样:
array (
'my-form' => array (
'name' => array (
'details' => array (
'avatar' => 'my-avatar.png',
),
),
'type' => array (
'details' => array (
'avatar' => 'image/png',
),
),
'tmp_name' => array (
'details' => array (
'avatar' => 'phpmFLrzD',
),
),
'error' => array (
'details' => array (
'avatar' => 0,
),
),
'size' => array (
'details' => array (
'avatar' => 90996,
),
),
),
)
并且 getUploadedFiles() 返回的结构应该是:
array(
'my-form' => array(
'details' => array(
'avatar' => /* UploadedFileInterface instance */
),
),
)
在某些情况下,您可以指定一个文件数组:
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 (
'name' => array (
'details' => array (
'avatars' => array (
0 => 'my-avatar.png',
1 => 'my-avatar2.png',
2 => 'my-avatar3.png',
),
),
),
'type' => array (
'details' => array (
'avatars' => array (
0 => 'image/png',
1 => 'image/png',
2 => 'image/png',
),
),
),
'tmp_name' => array (
'details' => array (
'avatars' => array (
0 => 'phpmFLrzD',
1 => 'phpV2pBil',
2 => 'php8RUG8v',
),
),
),
'error' => array (
'details' => array (
'avatars' => array (
0 => 0,
1 => 0,
2 => 0,
),
),
),
'size' => array (
'details' => array (
'avatars' => array (
0 => 90996,
1 => 90996,
3 => 90996,
),
),
),
),
)
上面的$_FILES数组将对应于 getUploadedFiles() 返回的以下结构:
array(
'my-form' => array(
'details' => array(
'avatars' => array(
0 => /* UploadedFileInterface instance */,
1 => /* UploadedFileInterface instance */,
2 => /* UploadedFileInterface instance */,
),
),
),
)
消费者将使用以下方法访问嵌套数组的索引 1:
$request->getUploadedFiles()['my-form']['details']['avatars'][1];
因为上传的文件数据是派生的(派生自请求正文或请求正文),所以接口还有一个设置方法 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 环境中运行。因此,UploadedFileInterface提供了确保操作在任何环境下都能正常工作的方法。尤其是:
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);
- 包
接口和类的描述作为 psr/http-message 包的一部分提供。
- 接口
**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 static
*/
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 static
* @throws \InvalidArgumentException 无效的报头字段或报头信息时抛出
*/
public function withHeader($name, $value);
/**
* 返回一个报头信息增量的 HTTP 消息实例
*
* 原有的报头信息会被保留,新的值会作为增量加上,如果报头信息不存在的话,字段会被加上
*
* 此方法在实现的时候,必须保留原有的不可修改的 HTTP 消息对象,
* 然后返回一个新的修改过的 HTTP 消息实例
*
* @param string $name 不区分大小写的报头字段名称
* @param string|string[] $value 报头信息或报头信息数组
* @return static
* @throws \InvalidArgumentException 报头字段名称非法时会被抛出
* @throws \InvalidArgumentException 报头头信息的值非法的时候会被抛出
*/
public function withAddedHeader($name, $value);
/**
* 返回被移除掉指定报头信息的 HTTP 消息实例
*
* 报头信息字段在解析的时候,必须保证是不区分大小写的
*
* 此方法在实现的时候,必须保留原有的不可修改的 HTTP 消息对象,
* 然后返回一个新的修改过的 HTTP 消息实例
*
* @param string $name 不区分大小写的头部字段名称
* @return static
*/
public function withoutHeader($name);
/**
* 获取 HTTP 消息的内容
*
* @return StreamInterface 以数据流的形式返回
*/
public function getBody();
/**
* 返回指定内容的 HTTP 消息实例
*
* 内容必须是 `StreamInterface` 接口的实例
*
* 此方法在实现的时候,必须保留原有的不可修改的 HTTP 消息对象,
* 然后返回一个新的修改过的 HTTP 消息实例
*
* @param StreamInterface $body 数据流形式的内容
* @return static
* @throws \InvalidArgumentException 当消息内容不正确的时候抛出
*/
public function withBody(StreamInterface $body);
}
**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-5.3
* (关于请求目标的各种允许的格式)
* @param mixed $requestTarget
* @return static
*/
public function withRequestTarget($requestTarget);
/**
* 获取当前请求使用的 HTTP 方法
*
* @return string HTTP 方法字符串
*/
public function getMethod();
/**
* 返回更改了请求方法的消息实例
*
* 虽然,在大部分情况下,HTTP 请求方法都是使用大写字母来标示的,
* 但是,实现类库不应该修改用户传参的大小格式
*
* 此方法在实现的时候,必须保留原有的不可修改的 HTTP 请求实例,
* 然后返回一个新的修改过的 HTTP 请求实例
*
* @param string $method 大小写敏感的方法名
* @return static
* @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 static
*/
public function withUri(UriInterface $uri, $preserveHost = false);
}
**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 static
*/
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 static
*/
public function withQueryParams(array $query);
/**
* 获取规范化的上传文件数据
*
* 这个方法会规范化返回的上传文件元数据树结构,
* 每个叶子结点都是 `Psr\Http\Message\UploadedFileInterface` 实例
*
* 这些值可能在实例化的时候从 `$_FILES` 或消息体中获取,
* 或者通过 `withUploadedFiles()` 获取
*
* @return array `UploadedFileInterface` 的实例数组;如果没有数据则必须返回一个空数组
*/
public function getUploadedFiles();
/**
* 返回使用指定的上传文件数据的新实例
*
* 此方法在实现的时候,必须保留原有的不可修改的 HTTP 消息实例,
* 然后返回一个新的修改过的 HTTP 消息实例
*
* @param array $uploadedFiles `UploadedFileInterface` 实例的树结构,类似于 `getUploadedFiles()` 的返回值
* @return static
* @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 static
* @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 static
*/
public function withAttribute($name, $value);
/**
* 返回移除指定属性的实例
*
* 此方法允许移除 getAttributes() 中声明的单个派生的请求属性
*
* 此方法在实现的时候,必须保留原有的不可修改的 HTTP 消息实例,
* 然后返回一个新的修改过的 HTTP 消息实例
*
* @see getAttributes()
* @param string $name 属性名
* @return static
*/
public function withoutAttribute($name);
}
**Psr\Http\Message\ResponseInterface**
<?php
namespace Psr\Http\Message;
/**
* 表示服务器返回的响应消息
*
* 根据 HTTP 规范,此接口包含以下各项的属性:
*
* - 协议版本
* - 状态码和原因短语
* - 报头
* - 消息体
*
* HTTP 响应是被视为无法修改的,所有能修改状态的方法,都必须有一套机制,
* 在内部保持好原有的内容,然后把修改状态后的,新的 HTTP 响应实例返回
*/
interface ResponseInterface extends MessageInterface
{
/**
* 获取响应状态码
*
* 状态码是一个三位整数,用于理解请求
*
* @return int Status code.
*/
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 static
* @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();
}
**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);
}
**Psr\Http\Message\UriInterface**
<?php
namespace Psr\Http\Message;
/**
* URI 数据对象
*
* 此接口按照 RFC 3986 来构建 HTTP URI,提供了一些通用的操作,
* 你可以自由的对此接口进行扩展。你可以使用此 URI 接口来做 HTTP 相关的操作,
* 也可以使用此接口做任何 URI 相关的操作。
*
* 此接口的实例化对象被视为无法修改的,所有能修改状态的方法,都必须有一套机制,
* 在内部保持好原有的内容,然后把修改状态后的,新的实例返回。
*
* 通常,HOST 信息也将出现在请求消息中。对于服务器端的请求,通常可以在服务器参数中发现此信息
*
* @see http://tools.ietf.org/html/rfc3986 (URI 通用标准规范)
*/
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 static 具有指定 Scheme 的新实例
* @throws \InvalidArgumentException 使用无效的 Scheme 时抛出
* @throws \InvalidArgumentException 使用不支持的 Scheme 时抛出
*/
public function withScheme($scheme);
/**
* 返回具有指定用户信息的实例
*
* 此方法必须保留当前实例的状态,并返回包含指定用户信息的实例
*
* 密码是可选的,但用户信息必须包括用户;用户信息的空字符串相当于删除用户信息
*
* @param string $user 用于认证的用户名
* @param null|string $password 密码
* @return static 具有指定用户信息的新实例
*/
public function withUserInfo($user, $password = null);
/**
* 返回具有指定 HOST 信息的实例
*
* 此方法必须保留当前实例的状态,并返回包含指定 HOST 信息的实例
*
* 空的 HOST 信息等同于删除 HOST 信息
*
* @param string $host 用于新实例的 HOST 信息
* @return static 具有指定 HOST 信息的实例
* @throws \InvalidArgumentException 使用无效的 HOST 信息时抛出
*/
public function withHost($host);
/**
* 返回具有指定端口的实例
*
* 此方法必须保留当前实例的状态,并返回包含指定端口的实例
*
* 实现必须为已建立的 TCP 和 UDP 端口范围之外的端口引发异常
*
* 为端口提供的空值等同于删除端口信息
*
* @param null|int $port 用于新实例的端口;`null` 值将删除端口信息
* @return static 具有指定端口的实例
* @throws \InvalidArgumentException 使用无效端口时抛出异常
*/
public function withPort($port);
/**
* 返回具有指定路径的实例
*
* 此方法必须保留当前实例的状态,并返回包含指定路径的实例
*
* 路径可以是空的、绝对的(以斜线开头)或者相对路径(不以斜线开头),实现必须支持这三种语法
*
* 如果 HTTP 路径旨在与 HOST 相对而不是路径相对,那么它必须以斜线开头
* 假设 HTTP 路径不以斜线开头,对应用程序或开发人员来说,相对于一些已知的路径。
*
* 用户可以提供编码和解码的路径字符,要确保实现了 `getPath()` 中描述的正确编码
*
* @param string $path 用于新实例的路径
* @return static 具有指定路径的实例
* @throws \InvalidArgumentException 使用无效的路径时抛出
*/
public function withPath($path);
/**
* 返回具有指定查询字符串的实例
*
* 此方法必须保留当前实例的状态,并返回包含查询字符串的实例
*
* 用户可以提供编码和解码的查询字符串,要确保实现了 `getQuery()` 中描述的正确编码
*
* 空查询字符串值等同于删除查询字符串
*
* @param string $query 用于新实例的查询字符串
* @return static 具有指定查询字符串的实例
* @throws \InvalidArgumentException 使用无效的查询字符串时抛出
*/
public function withQuery($query);
/**
* 返回具有指定 URI 片段(Fragment)的实例
*
* 此方法必须保留当前实例的状态,并返回包含片段的实例
*
* 用户可以提供编码和解码的片段,要确保实现了 `getFragment()` 中描述的正确编码
*
* 空片段值等同于删除片段
*
* @param string $fragment 用于新实例的片段
* @return static 具有指定 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();
}
**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-7:HTTP 消息接口 - 说明文档
- 概述
该提案的目的是为 RFC 7230 和 RFC 7231 中描述的 HTTP 消息以及 RFC 3986 中描述的 URI (在 HTTP 消息的上下文中)提供一组通用接口。
- RFC 7230:http://www.ietf.org/rfc/rfc7230.txt
- RFC 7231:http://www.ietf.org/rfc/rfc7231.txt
- RFC 3986:http://www.ietf.org/rfc/rfc3986.txt
所有 HTTP 消息都包含正在使用的 HTTP 协议版本、标头和消息正文。请求建立在消息的基础上,包括用于发出请求的 HTTP 方法以及发出请求的 URI。响应包括 HTTP 状态代码和原因短语。
在 PHP 中,HTTP 消息在两种情况下使用:
- 发送 HTTP 请求,通过
ext/curl扩展、PHP 的原生流层等,对收到的 HTTP 响应进行处理。换句话说,当使用 PHP 作为 HTTP 客户端时使用 HTTP 消息。 - 处理对服务器的传入 HTTP 请求,并向发出请求的客户端返回 HTTP 响应。当 PHP 用作服务器端应用程序来满足 HTTP 请求时,可以使用 HTTP 消息。
该提案提供了一个 API,用于完整描述 PHP 中各种 HTTP 消息的所有部分。
- PHP 中的 HTTP 消息
PHP 没有对 HTTP 消息的内置支持。
客户端 HTTP 支持
PHP 支持通过多种机制发送 HTTP 请求:
PHP 流是发送 HTTP 请求的最方便和普遍的方式,但在正确配置 SSL 支持方面存在许多限制,并提供了一个繁琐的接口来设置诸如标头之类的内容。cURL 提供了完整且扩展的功能集,但由于它不是默认扩展,因此通常不存在。http 扩展存在与 cURL 相同的问题,以及传统上它的使用示例要少得多的事实。
大多数现代 HTTP 客户端库倾向于抽象实现,以确保它们可以在任何执行它们的环境中工作,并且可以跨越上述任何层。
服务器端 HTTP 支持
PHP 使用服务器 API (SAPI) 来解释传入的 HTTP 请求、编组输入并将处理传递给脚本。最初的 SAPI 设计反映了Common Gateway Interface,它会在将委托传递给脚本之前编组请求数据并将其推送到环境变量中;然后脚本将从环境变量中提取以处理请求并返回响应。
PHP 的 SAPI 设计通过超全局变量(分别为$_GET、$_POST 和 $_COOKIE)抽象出常见的输入源,例如 cookie、查询字符串参数和 url 编码的 POST 内容,为 Web 开发人员提供了一层便利。
在等式的响应方面,PHP 最初是作为模板语言开发的,允许混合 HTML 和 PHP;文件的任何 HTML 部分都会立即刷新到输出缓冲区。现代应用程序和框架避开了这种做法,因为它可能导致与发出状态行或响应标头有关的问题;它们倾向于聚合所有标头和内容,并在所有其他应用程序处理完成时立即发出它们。需要特别注意确保将内容发送到输出缓冲区的错误报告和其他操作不会刷新输出缓冲区。
- 为什么需要它?
HTTP 消息用于大量 PHP 项目——客户端和服务器。在每种情况下,我们都会观察到以下一种或多种模式或情况:
- 项目直接使用 PHP 的超全局变量。
- 项目将从头开始创建实现。
- 项目可能需要提供 HTTP 消息实现的特定 HTTP 客户端/服务器库。
- 项目可以为常见的 HTTP 消息实现创建适配器。
例如:
- 几乎所有在框架兴起之前就开始开发的应用程序,包括许多非常流行的 CMS、论坛和购物车系统,在历史上都使用过超全局变量。
- Symfony 和 Zend Framework 等框架都定义了构成其 MVC 层基础的 HTTP 组件;甚至像 oauth2-server-php 这样的小型、单一用途的库也提供并需要它们自己的 HTTP 请求/响应实现。Guzzle、Buzz 和其他 HTTP 客户端实现也各自创建自己的 HTTP 消息实现。
- Silex、Stack 和 Drupal 8 等项目对 Symfony 的 HTTP 内核有很强的依赖性。任何基于 Guzzle 构建的 SDK 对 Guzzle 的 HTTP 消息实现都有严格的要求。
- Geocoder 等项目为通用库创建了冗余适配器。
直接使用超全局变量有很多问题。首先,这些是可变的,这使得库和代码可以改变值,从而改变应用程序的状态。此外,超全局变量使单元测试和集成测试变得困难和脆弱,导致代码质量下降。
在当前实现 HTTP 消息抽象的框架生态系统中,最终结果是项目不具备互操作性或交叉授权能力。为了从另一个框架使用针对一个框架的代码,首要任务是在 HTTP 消息实现之间构建一个桥接层。在客户端,如果特定库没有可以使用的适配器,如果您希望使用来自另一个库的适配器,则需要桥接请求/响应对。
最后,当涉及到服务器端响应时,PHP 有自己的方式:header() 在调用之前发出的任何内容都会导致该调用变为无操作;根据错误报告设置,这通常意味着标头或响应状态未正确发送。解决此问题的一种方法是使用 PHP 的输出缓冲功能,但输出缓冲区的嵌套可能会出现问题并且难以调试。因此,框架和应用程序倾向于创建响应抽象来聚合可以一次发出的标头和内容——而这些抽象通常是不兼容的。
因此,该提案的目标是抽象客户端和服务器端的请求和响应接口,以促进项目之间的互操作性。如果项目实现了这些接口,则在采用来自不同库的代码时可以假定具有合理的兼容性水平。
应该注意的是,该提案的目标不是要淘汰现有 PHP 库使用的当前接口。该提案旨在实现 PHP 包之间的互操作性,以便描述 HTTP 消息。
- 范围
目标
- 提供描述 HTTP 消息所需的接口。
- 专注于实际应用和可用性。
- 定义接口以对 HTTP 消息和 URI 规范的所有元素进行建模。
- 确保 API 不对 HTTP 消息施加任意限制。例如,一些 HTTP 消息体可能太大而无法存储在内存中,因此我们必须考虑到这一点。
- 为处理服务器端应用程序的传入请求和在 HTTP 客户端中发送传出请求提供有用的抽象。
非目标
- 该提案并不期望所有 HTTP 客户端库或服务器端框架都更改其接口以符合要求。它严格用于互操作性。
- 虽然每个人对实现细节的看法各不相同,但本提案不应强加实现细节。由于 RFC 7230、7231 和 3986 不强制任何特定的实现,因此需要一定数量的发现来描述 PHP 中的 HTTP 消息接口。
- 设计决策
消息设计MessageInterface 为所有 HTTP 消息共有的元素提供访问器,无论它们是用于请求还是响应。这些要素包括:
- HTTP 协议版本(例如,“1.0”、“1.1”)
- HTTP 标头
- HTTP 消息体
更具体的接口用于描述请求和响应,更具体地说是每个接口的上下文(客户端与服务器端)。这些划分部分受到现有 PHP 使用的启发,但也受到其他语言的启发,例如 Ruby 的Rack、Python 的WSGI、Go 的http 包、Node 的http 模块等。
为什么消息上有标头方法而不是标头包?
消息本身是标头(以及其他消息属性)的容器。这些如何在内部表示是一个实现细节,但对标头的统一访问是消息的责任。
为什么 URI 被表示为对象?
URI 是值,其身份由值定义,因此应建模为值对象。
此外,URI 包含在给定请求中可能被多次访问的各种段——并且需要解析 URI 才能确定(例如,通过parse_url())。将 URI 建模为值对象只允许解析一次,并简化了对各个段的访问。它还通过允许用户创建仅具有更改的段的基本 URI 实例的新实例(例如,仅更新路径)来为客户端应用程序提供便利。
为什么请求接口有处理请求目标和组成 URI 的方法?
RFC 7230 将请求行详细说明为包含“请求目标”。在 request-target 的四种形式中,只有一种是符合 RFC 3986 的 URI;最常用的形式是 origin-form,它表示没有方案或权限信息的 URI。此外,由于所有表格对于请求的目的都是有效的,因此提案必须适应每一个。RequestInterface因此具有与请求目标相关的方法。默认情况下,它将使用组合的 URI 来呈现原始形式的请求目标,并且在没有 URI 实例的情况下,返回字符串“/”。另一种方法, withRequestTarget()允许指定具有特定请求目标的实例,允许用户创建使用其他有效请求目标表单之一的请求。
出于各种原因,URI 被保留为请求的离散成员。对于客户端和服务器,通常需要了解绝对 URI。在客户端的情况下,需要 URI,特别是方案和权限详细信息,才能建立实际的 TCP 连接。对于服务器端应用程序,通常需要完整的 URI 才能验证请求或路由到适当的处理程序。
为什么要价值对象?
该提案将消息和 URI 建模为值对象。
消息是标识是消息所有部分的聚合的值;对消息的任何方面的更改本质上是一条新消息。这就是价值对象的定义。更改导致新实例的做法称为不变性,是一种旨在确保给定值完整性的功能。
该提案还认识到,大多数客户端和服务器端应用程序将需要能够轻松更新消息方面,并因此提供将使用更新创建新消息实例的接口方法。这些通常以with或 without.
在对 HTTP 消息进行建模时,值对象提供了几个好处:
- URI 状态的变化不能改变构成 URI 实例的请求。
- 标头的更改不会改变组成它们的消息。
本质上,将 HTTP 消息建模为值对象可确保消息状态的完整性,并防止需要双向依赖,这通常会不同步或导致调试或性能问题。
对于 HTTP 客户端,它们允许消费者使用基本 URI 和所需标头等数据构建基本请求,而无需为客户端发送的每条消息构建全新的请求或重置请求状态:
$uri = new Uri('http://api.example.com');
$baseRequest = new Request($uri, null, [
'Authorization' => 'Bearer ' . $token,
'Accept' => 'application/json',
]);
$request = $baseRequest->withUri($uri->withPath('/user'))->withMethod('GET');
$response = $client->sendRequest($request);
// get user id from $response
$body = new StringStream(json_encode(['tasks' => [
'Code',
'Coffee',
]]));
$request = $baseRequest
->withUri($uri->withPath('/tasks/user/' . $userId))
->withMethod('POST')
->withHeader('Content-Type', 'application/json')
->withBody($body);
$response = $client->sendRequest($request)
// No need to overwrite headers or body!
$request = $baseRequest->withUri($uri->withPath('/tasks'))->withMethod('GET');
$response = $client->sendRequest($request);
在服务器端,开发人员需要:
- 反序列化请求消息正文。
- 解密 HTTP cookie。
- 返回响应。
这些操作也可以用值对象来完成,有很多好处:
- 可以存储原始请求状态以供任何消费者检索。
- 可以使用默认标头或消息正文创建默认响应状态。
当今最流行的 PHP 框架具有完全可变的 HTTP 消息。使用价值对象所需的主要更改是:
- 不会调用 setter 方法或设置公共属性,而是会调用 mutator 方法并分配结果。
- 开发人员必须将状态更改通知应用程序。
例如,在 Zend Framework 2 中,而不是以下内容:
function (MvcEvent $e)
{
$response = $e->getResponse();
$response->setHeaderLine('x-foo', 'bar');
}
现在有人会写:
function (MvcEvent $e)
{
$response = $e->getResponse();
$e->setResponse(
$response->withHeader('x-foo', 'bar')
);
}
以上将分配和通知结合在一个消息中。
这种做法的一个附带好处是明确地对正在进行的应用程序状态进行任何更改。
新实例与返回 $this
对各种方法的一个观察是,如果所提出的论点不会导致值的变化,它们可能会安全。这样做的一个理由是性能(因为这不会导致克隆操作)。
各种接口都写有指示必须保留不变性的措施,但仅指示必须返回包含新状态的“实例”。由于表示相同值的实例被认为是相等的,因此返回$this在功能上是等效的,因此是允许的。
使用流而不是 X MessageInterface使用必须实现的主体值StreamInterface。做出此设计决策是为了让开发人员可以发送和接收(或接收和发送)HTTP 消息,这些消息包含的数据比实际存储在内存中的数据多,同时仍允许以字符串形式与消息体进行交互。虽然 PHP 通过流包装器提供流抽象,但流资源使用起来可能很麻烦:流资源只能使用stream_get_contents()或手动读取字符串的其余部分转换为字符串。在使用或填充流时向流添加自定义行为需要注册流过滤器;但是,流过滤器只能在过滤器注册到 PHP 之后添加到流中(即,没有流过滤器自动加载机制)。
使用定义良好的流接口允许灵活的流装饰器的潜力,可以将其添加到请求或响应运行前以启用加密、压缩等功能,确保下载的字节数反映报告的字节数在响应中。装饰流是 Java 和 Node 社区中一种成熟的模式, 它允许非常灵活的流。StreamInterface 大部分 API 是基于 Python 的 io 模块,它提供了一个实用且可消费的 API。WritableStreamInterface流的功能不是使用 isReadable() 和 isWritable()之类的东西来实现流功能,而是ReadableStreamInterface由isReadable(),isWritable()等方法提供。Python、 C#、C++、 Ruby、 Node 和可能的其他方法都使用这种方法。
如果我只想返回一个文件怎么办?
在某些情况下,您可能希望从文件系统返回一个文件。在 PHP 中执行此操作的典型方法是以下之一:
readfile($filename);
stream_copy_to_stream(fopen($filename, 'r'), fopen('php://output', 'w'));
请注意,上面省略了发送适当的Content-Type和 Content-Length标头;开发人员需要在调用上述代码之前发出这些。
使用 HTTP 消息的等效方法是使用StreamInterface 接受文件名或流资源的实现,并将其提供给响应实例。一个完整的示例,包括设置适当的标题:
// where Stream is a concrete StreamInterface:
$stream = new Stream($filename);
$finfo = new finfo(FILEINFO_MIME);
$response = $response
->withHeader('Content-Type', $finfo->file($filename))
->withHeader('Content-Length', (string) filesize($filename))
->withBody($stream);
发出此响应会将文件发送到客户端。
如果我想直接发出输出怎么办?
直接发出输出(例如通过echo、printf或写入 php://output流)通常仅作为性能优化或在发出大型数据集时是可取的。如果需要完成,并且您仍希望在 HTTP 消息范式中工作,则根据此示例,一种方法是使用基于回调的实现。将任何发出输出的代码直接包装在回调中,将其传递给适当的 StreamInterface 实现,并将其提供给消息正文:
$output = new CallbackStream(function () use ($request) {
printf("The requested URI was: %s<br>\n", $request->getUri());
return '';
});
return (new Response())
->withHeader('Content-Type', 'text/html')
->withBody($output);
如果我想对内容使用迭代器怎么办?
Ruby 的 Rack 实现对服务器端响应消息体使用基于迭代器的方法。这可以通过迭代器支持的方法使用 HTTP 消息范例来模拟,如psr7examples 存储库中所述。
为什么流是可变的?StreamInterfaceAPI 包括诸如write()可以更改消息内容的方法——这与具有不可变消息直接矛盾。
出现的问题是由于该接口旨在包装 PHP 流或类似的事情。因此,写入操作将代理写入流。即使我们使StreamInterface不可变,一旦流被更新,任何包装该流的实例也将被更新——这使得不可变不可能强制执行。
我们的建议是实现对服务器端请求和客户端响应使用只读流。
ServerRequestInterface 的基本原理RequestInterface和 RFC 7230 中描述的ResponseInterface请求和响应消息具有本质上 1:1 的相关性 。它们提供接口来实现对应于它们建模的特定 HTTP 消息类型的值对象。
对于服务器端应用程序,传入请求还有其他注意事项:
- 访问服务器参数(可能来自请求,也可能是服务器配置的结果,通常通过超全局
$_SERVER表示;这些是 PHP Server API (SAPI) 的一部分)。 - 访问查询字符串参数(通常通过超全局
$_GET封装在 PHP 中)。 - 访问已解析的主体(即从传入请求主体反序列化的数据;在 PHP 中,这通常是使用
application/x-www-form-urlencoded内容类型的 POST 请求的结果,并封装在超全局$_POST中,但对于非 POST、非表单编码的数据,可以是数组或对象)。 - 访问上传的文件(通过超全局
$_FILES封装在 PHP 中)。 - 访问 cookie 值(通过超全局
$_COOKIE封装在 PHP 中)。 - 访问从请求派生的属性(通常但不限于与 URL 路径匹配的属性)。
对这些参数的统一访问增加了框架和库之间互操作性的可行性,因为他们现在可以假设如果请求实现ServerRequestInterface,他们可以获得这些值。它还解决了 PHP 语言本身的问题:
- 直到 5.6.0,
php://input被读取一次;因此,从多个框架/库实例化多个请求实例可能会导致状态不一致,因为第一个访问php://input的将是唯一接收数据的。 - 针对超全局变量(例如
$_GET、$_FILES等)的单元测试很困难并且通常很脆弱。考虑将它们封装在ServerRequestInterface中实现可以简化测试。
为什么在 ServerRequestInterface 中“解析体”?
有人提出使用术语“BodyParams”的参数,并要求该值是一个数组,其基本原理如下:
- 与其他服务器端参数访问保持一致。
$_POST是一个数组,并且 80% 的用例将针对该超全局。- 单一类型可以实现强契约,简化使用。
主要论点是,如果主体参数是一个数组,开发人员可以对值进行可预测的访问:
$foo = isset($request->getBodyParams()['foo'])
? $request->getBodyParams()['foo']
: null;
使用“解析体”的论点是通过检查域提出的。消息体实际上可以包含任何内容。虽然传统的 Web 应用程序使用表单并使用 POST 提交数据,但这是一个在当前 Web 开发趋势中迅速受到挑战的用例,这些趋势通常以 API 为中心,因此也使用备用请求方法(尤其是 PUT 和 PATCH)作为非格式编码的内容(通常是 JSON 或 XML),在许多情况下可以强制转换为数组,但在许多情况下也不能或不应该。
如果强制表示解析体的属性只是一个数组,则开发人员需要一个共享约定,将解析体的结果放在哪里。这些可能包括:
- 正文参数下的特殊键,例如
__parsed__. - 一个特殊命名的属性,例如
__body__.
最终结果是开发人员现在必须查看多个位置:
$data = $request->getBodyParams();
if (isset($data['__parsed__']) && is_object($data['__parsed__'])) {
$data = $data['__parsed__'];
}
// or:
$data = $request->getBodyParams();
if ($request->hasAttribute('__body__')) {
$data = $request->getAttribute('__body__');
}
提出的解决方案是使用术语“ParsedBody”,这意味着这些值是解析消息正文的结果。这也意味着返回值将是模棱两可的;但是,因为这是域的属性,所以这也是意料之中的。因此,用法将变为:
$data = $request->getParsedBody();
if (! $data instanceof \stdClass) {
// raise an exception!
}
// otherwise, we have what we expected
这种方法消除了强制数组的限制,代价是返回值的模糊性。考虑到其他建议的解决方案——将解析的数据推送到特殊的主体参数键或属性中——也存在歧义,因此建议的解决方案更简单,因为它不需要添加接口规范。最终,在表示解析正文的结果时,歧义性实现了所需的灵活性。
为什么没有包含用于检索“基本路径”的功能?
许多框架提供了获取“基本路径”的能力,通常被认为是到达并包括前端控制器的路径。例如,如果应用程序在 http://example.com/b2b/index.php 提供服务,并且用于请求它的当前 URI 是http://example.com/b2b/index.php/customer/register,则检索基本路径的功能将返回/b2b/index.php。然后,路由可以使用此值在尝试匹配之前剥离该路径。
该值通常也用于应用程序中的 URI 生成;参数将传递给路由,路由将生成路径,并在其前面加上基本路径,以返回完全限定的 URI。其他工具(通常是视图助手、模板过滤器或模板函数)用于解析相对于基本路径的路径,以生成用于链接到静态资产等资源的 URI。
在检查几种不同的实现时,我们注意到以下几点:
- 用于确定基本路径的逻辑在不同的实现之间变化很大。例如,将 ZF2 中的逻辑与 Symfony 2 中的逻辑进行比较。
- 大多数实现似乎允许手动注入到路由的基本路径或用于 URI 生成的任何措施。
- 主要用例——路由和 URI 生成——通常是功能的唯一消费者;开发人员通常不需要了解基本路径概念,因为其他对象会为他们处理这些细节。例如:
- 路由会在路由过程中为您剥离基本路径;您不需要将修改后的路径传递给路由。
- 视图助手、模板过滤器等通常在调用之前注入基本路径。有时这是手动完成的,但更多时候是框架接线的结果。
- 通过服务器参数和 URI 实例,计算基本路径所需的所有资源都已经在
RequestInterface实例中。
我们的立场是,基本路径检测是特定于框架或应用程序的,检测结果可以很容易地注入到需要它的对象中,和或使用RequestInterface实例本身的实用程序函数或类根据需要进行计算。
为什么 getUploadedFiles() 返回对象而不是数组?
这样做主要是为了简化规范:我们指定一个接口,而不是要求数组的实现。
此外,UploadedFileInterface中的数据经过标准化,可以在 SAPI 和非 SAPI 环境中工作。这允许创建进程来手动解析消息正文并将内容分配给流,而无需先写入文件系统,同时仍然允许在 SAPI 环境中正确处理文件上传。
“特殊”标头值呢?
许多标头值包含独特的表示要求,这可能会给消费和生成带来问题;特别是 cookie 和Accept标头。
该提案不提供对任何标头类型的任何特殊处理。baseMessageInterface提供了 header 检索和设置的方法,所有 header 值最终都是字符串值。
鼓励开发人员编写用于与这些标头值交互的类库,用于解析或生成。然后,用户可以在需要与这些值交互时使用这些库。这种做法的示例已经存在于 willdurand/Negotiation 和 Aura.Accept 等库中。只要对象具有将值转换为字符串的功能,这些对象就可以用于填充 HTTP 消息的标头。
PSR-15:HTTP 请求处理
本文档描述了 HTTP 服务器请求处理程序(“请求处理程序”)和 HTTP 服务器中间件组件(“中间件”)的通用接口,它们使用 PSR-7 或后续替代 PSR 所描述的 HTTP 消息。
HTTP 请求处理程序是任何 Web 应用程序的基本部分。服务器端代码接收请求消息,对其进行处理并生成响应消息。HTTP 中间件是一种将通用请求和响应处理从应用层分离的方法。
本文档中描述的接口是请求处理程序和中间件的抽象。
注意:所有对“请求处理程序”和“中间件”的引用都特定于服务器请求处理。
- 规范
请求处理程序
请求处理程序是处理请求并产生响应的单个组件,如 PSR-7 所定义。
如果请求条件阻止它产生响应,请求处理程序可能会抛出异常。未定义异常类型。
使用此标准的请求处理程序必须实现以下接口:
Psr\Http\Server\RequestHandlerInterface
Middleware
中间件组件是一个单独的组件,通常与其他中间件组件一起参与处理传入请求和创建结果响应,如 PSR-7 所定义。
如果满足足够的条件,中间件组件可以创建并返回响应而不委托给请求处理程序。
使用此标准的中间件必须实现以下接口:
Psr\Http\Server\MiddlewareInterface
生成响应
建议任何生成响应的中间件或请求处理程序将组成 PSR-7 ResponseInterface的原型或能够生成ResponseInterface实例的工厂,以防止依赖于特定的 HTTP 消息实现。
处理异常
建议任何使用中间件的应用程序都包含一个捕获异常并将其转换为响应的组件。该中间件应该是第一个执行的组件,并包装所有进一步的处理以确保始终生成响应。
- 接口
**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;
}
**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-15:HTTP 请求处理 - 说明文档
- 概述
此 PSR 的目的是为 HTTP 服务器请求处理程序(“请求处理程序”)和 HTTP 服务器请求中间件(“中间件”)定义与 PSR-7 或后续替代 PSR 中定义的 HTTP 消息兼容的正式接口。
注意:所有对“请求处理程序”和“中间件”的引用都特定于服务器请求处理。
- 为什么需要它?
HTTP 消息规范不包含对请求处理程序或中间件的任何引用。
请求处理程序是任何 Web 应用程序的基本部分。处理程序是接收请求并产生响应的组件。几乎所有处理 HTTP 消息的代码都会有某种请求处理程序。
中间件在 PHP 生态系统中已经存在多年。StackPHP普及了可重用中间件的一般概念。自从 HTTP 消息作为 PSR 发布以来,许多框架都采用了使用 HTTP 消息接口的中间件。
就正式的请求处理程序和中间件接口达成一致消除了几个问题并有许多好处:
- 为开发人员提供了一个正式的标准。
- 使任何中间件组件能够在任何兼容的框架中运行。
- 消除了由各种框架定义的类似接口的重复。
- 避免方法签名中的细微差异。
- 范围
目标
- 创建一个使用 HTTP 消息的请求处理程序接口。
- 创建一个使用 HTTP 消息的中间件接口。
- 实现基于最佳实践的请求处理程序和中间件签名。
- 确保请求处理程序和中间件与 HTTP 消息的任何实现兼容。
非目标
- 试图定义创建 HTTP 响应的机制。
- 试图为客户端/异步中间件定义接口。
- 试图定义如何分发中间件。
- 请求处理方法
有许多使用 HTTP 消息的请求处理程序的方法。但是,它们的一般过程是相同的:
给定一个 HTTP 请求,为该请求生成一个 HTTP 响应。
该过程的内部要求因框架和应用程序而异。该提案没有努力确定该过程应该是什么。
- 中间件方法
目前有两种常用的中间件方法使用 HTTP 消息。
双通
大多数中间件实现使用的签名基本相同,并且基于Express 中间件,其定义为:
fn(request, response, next): response
基于采用此签名的框架已经使用的中间件实现,可以观察到以下共性:
- 中间件被定义为可调用的。
- 中间件在调用期间传递了 3 个参数:
- 一个
ServerRequestInterface实现。 - 一个
ResponseInterface实现。 callable接收请求和响应以委托给下一个中间件。
- 一个
大量项目提供或使用完全相同的界面。这种方法通常被称为“双重传递”,指的是请求和响应都传递给中间件。
使用双通道的项目
中间件实现双通
- bitexpert/adroit
- akrabat/rka-ip-address-middleware
- akrabat/rka-scheme-and-host-detection-middleware
- bear/middleware
- los/api-problem
- los/los-rate-limit
- monii/monii-action-handler-psr7-middleware
- monii/monii-nikic-fast-route-psr7-middleware
- monii/monii-response-assertion-psr7-middleware
- mtymek/blast-base-url
- ocramius/psr7-session
- oscarotero/psr7-middlewares
- php-middleware/block-robots
- php-middleware/http-authentication
- php-middleware/log-http-messages
- php-middleware/maintenance
- php-middleware/phpdebugbar
- php-middleware/request-id
- relay/middleware
这个接口的主要缺点是虽然接口本身是可调用的,但目前没有办法严格键入闭包。
单程 (Lambda)
中间件的另一种方法更接近StackPHP风格,定义为:
fn(request, next): response
采用这种方式的中间件一般有以下共性:
- 中间件使用特定接口定义,该接口带有一个接受请求进行处理的方法。
- 中间件在调用期间传递了 2 个参数:
- HTTP 请求消息。
- 一个请求处理程序,中间件可以将生成 HTTP 响应消息的责任委托给该处理程序。
在这种形式中,中间件在请求处理程序生成响应之前无法访问响应。然后中间件可以在返回之前修改响应。
这种方法通常被称为“单次传递”或“lambda”,仅涉及传递给中间件的请求。
使用单通道的项目
在使用 HTTP 消息的项目中,这种方法的示例较少,但有一个明显的例外。
Guzzle 中间件专注于传出(客户端)请求并使用此签名:
function (RequestInterface $request, array $options): ResponseInterface
使用单通道的附加项目
也有一些重要的项目使用这种方法在 HTTP 消息之前进行。
StackPHP基于Symfony HttpKernel并支持具有此签名的中间件:
function handle(Request $request, $type, $catch): Response
注意:虽然 Stack 有多个参数,但不包括响应对象。
Laravel 中间件使用 Symfony 组件并支持具有此签名的中间件:
function handle(Request $request, callable $next): Response
方法比较
多年来,中间件的单通道方法已经在 PHP 社区中很好地建立起来。这在基于 StackPHP 的大量软件包中最为明显。
双通方法较新,但几乎已被 HTTP 消息 (PSR-7) 的早期采用者普遍使用。
选择的方法
尽管几乎普遍采用了双通道方法,但在实施方面存在重大问题。
最严重的是,传递一个空响应并不能保证响应处于可用状态。中间件可能会在将响应传递给进一步处理之前修改响应,这一事实进一步加剧了这种情况。
更复杂的问题是,没有办法确保响应主体没有被写入,这可能导致输出不完整或发送错误响应并附加缓存头。如果新内容比原始内容短,则在覆盖现有正文内容时,也可能导致正文内容损坏。解决这些问题的最有效方法是在修改消息正文时始终提供新流。
有些人认为传递响应有助于确保依赖倒置。虽然它确实有助于避免依赖于 HTTP 消息的特定实现,但也可以通过将工厂注入中间件以创建 HTTP 消息对象或注入空消息实例来解决问题。随着 PSR-17 中 HTTP 工厂的创建,处理依赖倒置的标准方法成为可能。
一个更主观但也很重要的问题是现有的双通道中间件通常使用callable类型提示来引用中间件。这使得严格类型变得不可能,因为不能保证callable 被传递的对象实现了中间件签名,这会降低运行时的安全性。
由于这些重大问题,该提案选择了 lambda 方法。
- 设计决策
请求处理程序设计RequestHandlerInterface定义了一个接受请求并必须返回响应的方法。请求处理程序可以委托给另一个处理程序。
为什么需要服务器请求?
为了明确请求处理程序只能在服务器端上下文中使用。在客户端上下文中,可能会返回一个承诺而不是响应。
为什么要使用“处理程序”一词?
术语“处理程序”是指指定用于管理或控制的事物。在请求处理方面,请求处理程序是必须对请求进行操作以创建响应的点。
与在本规范的先前版本中使用的术语“委托”相反,该接口的内部行为未指定。只要请求处理程序最终产生响应,它就是有效的。
为什么请求处理程序不使用**__invoke**?
使用__invoke比使用命名方法更不透明。它还使得在将请求处理程序分配给类变量时更容易调用它,而无需使用call_user_func或其他不太常见的语法。
中间件设计MiddlewareInterface定义了一个接受 HTTP 请求和请求处理程序并且必须返回响应的方法。中间件可以:
- 在将请求传递给请求处理程序之前改进请求。
- 在返回之前改进从请求处理程序收到的响应。
- 创建并返回响应而不将请求传递给请求处理程序,从而处理请求本身。
当按顺序从一个中间件委托给另一个中间件时,调度系统的一种方法是使用组成中间件序列的中间请求处理程序作为将中间件链接在一起的一种方式。最后的或最里面的中间件将充当应用程序代码的网关,并根据其结果生成响应;或者,中间件可以将此责任委托给专用的请求处理程序。
为什么不使用中间件**__invoke**?
这样做会与实现双通道方法的现有中间件发生冲突,并且可能希望实现中间件接口以实现与本规范的前向兼容性。
为什么叫这个名字**process()**?
我们审查了许多现有的单体和中间件框架,以确定每个为处理传入请求定义的方法。我们发现以下是常用的:
__invoke(在中间件系统中,例如 Slim、Expressive、Relay 等)handle(特别是源自 Symfony 的 HttpKernel 的软件)dispatch(Zend 框架的 DispatchableInterface)
我们选择允许此类类的前向兼容方法将它们自己重新定位为中间件(或与本规范兼容的中间件),因此需要选择一个不常用的名称。因此,我们选择 process, 来表示处理请求。
为什么需要服务器请求?
明确一点,中间件只能在同步的服务器端上下文中使用。
虽然并非所有中间件都需要使用服务器请求接口定义的附加方法,但出站请求通常是异步处理的,并且通常会返回响应的承诺。(这主要是因为多个请求可以并行发出并在返回时进行处理。)解决异步请求/响应生命周期的需求超出了本提案的范围。
此时尝试定义客户端中间件还为时过早。未来任何关注客户端请求处理的提案都应该有机会定义一个特定于异步中间件性质的标准。
请求处理程序的作用是什么?
中间件具有以下作用:
- 自行产生响应。如果满足特定的请求条件,中间件可以产生并返回响应。
- 返回请求处理程序的结果。在中间件不能产生自己的响应的情况下,它可以委托请求处理程序产生一个;有时这可能涉及提供转换后的请求(例如,注入请求属性或解析请求正文的结果)。
- 操作并返回请求处理程序产生的响应。在某些情况下,中间件可能对处理请求处理程序返回的响应感兴趣(例如,gzip 响应正文、添加 CORS 标头等)。在这种情况下,中间件将捕获请求处理程序返回的响应,并在完成时返回转换后的响应。
在后两种情况下,中间件可能有如下代码:
// Straight delegation:
return $handler->handle($request);
// Capturing the response to manipulate:
$response = $handler->handle($request);
处理程序的行为完全取决于开发人员,只要它产生响应即可。
在一个常见的场景中,处理程序在内部实现一个队列或一堆中间件实例。在这种情况下,调用 $handler->handle($request)将推进内部指针,拉取与该指针关联的中间件,并使用 $middleware->process($request, $this). 如果不再存在中间件,它通常会引发异常或返回预设响应。
另一种可能性是 将与传入服务器请求匹配的中间件路由到特定处理程序,然后返回执行该处理程序生成的响应。如果无法路由到处理程序,它将改为执行提供给中间件的处理程序。(这种机制甚至可以与中间件队列和堆栈结合使用。)
示例界面交互
两个接口RequestHandlerInterface和MiddlewareInterface被设计为相互配合工作。中间件在与任何总体应用层分离时获得了灵活性,而只依赖于提供的请求处理程序来产生响应。
下面展示了工作组观察或实施的两种中间件调度系统方法。此外,还提供了可重用中间件的示例来演示如何编写松耦合的中间件。
请注意,不建议将这些方法作为定义中间件调度系统的明确或唯一方法。
基于队列的请求处理程序
在这种方法中,请求处理程序维护一个中间件队列,如果队列用尽而没有返回响应,则返回一个回退响应。当执行第一个中间件时,队列将自己作为请求处理程序传递给中间件。
class QueueRequestHandler implements RequestHandlerInterface
{
private $middleware = [];
private $fallbackHandler;
public function __construct(RequestHandlerInterface $fallbackHandler)
{
$this->fallbackHandler = $fallbackHandler;
}
public function add(MiddlewareInterface $middleware)
{
$this->middleware[] = $middleware;
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
// Last middleware in the queue has called on the request handler.
if (0 === count($this->middleware)) {
return $this->fallbackHandler->handle($request);
}
$middleware = array_shift($this->middleware);
return $middleware->process($request, $this);
}
}
应用程序引导程序可能如下所示:
// Fallback handler:
$fallbackHandler = new NotFoundHandler();
// Create request handler instance:
$app = new QueueRequestHandler($fallbackHandler);
// Add one or more middleware:
$app->add(new AuthorizationMiddleware());
$app->add(new RoutingMiddleware());
// execute it:
$response = $app->handle(ServerRequestFactory::fromGlobals());
该系统有两个请求处理程序:一个用于在最后一个中间件委托给请求处理程序时产生响应,一个用于分派中间件层。(在此示例中,RoutingMiddleware可能会在成功的路由匹配上执行组合处理程序;请参阅下文。)
这种方法有以下好处:
- 中间件不需要了解任何其他中间件或其在应用程序中的组成方式。
- 这
QueueRequestHandler与使用中的 PSR-7 实现无关。 - 中间件按照添加到应用程序的顺序执行,使代码显式。
- “回退”响应的生成委托给应用程序开发人员。这允许开发人员确定这是否应该是“404 Not Found”条件、默认页面等。
基于装饰的请求处理程序
在这种方法中,请求处理程序实现装饰中间件实例和后备请求处理程序以传递给它。该应用程序是从外向内构建的,将每个请求处理程序“层”传递到下一个外部。
class DecoratingRequestHandler implements RequestHandlerInterface
{
private $middleware;
private $nextHandler;
public function __construct(MiddlewareInterface $middleware, RequestHandlerInterface $nextHandler)
{
$this->middleware = $middleware;
$this->nextHandler = $nextHandler;
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->middleware->process($request, $this->nextHandler);
}
}
// Create a response prototype to return if no middleware can produce a response
// on its own. This could be a 404, 500, or default page.
$responsePrototype = (new Response())->withStatus(404);
$innerHandler = new class ($responsePrototype) implements RequestHandlerInterface {
private $responsePrototype;
public function __construct(ResponseInterface $responsePrototype)
{
$this->responsePrototype = $responsePrototype;
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->responsePrototype;
}
};
$layer1 = new DecoratingRequestHandler(new RoutingMiddleware(), $innerHandler);
$layer2 = new DecoratingRequestHandler(new AuthorizationMiddleware(), $layer1);
$response = $layer2->handle(ServerRequestFactory::fromGlobals());
与基于队列的中间件类似,请求处理程序在该系统中有两个用途:
- 如果没有其他层这样做,则产生回退响应。
- 调度中间件。
可重用中间件示例
在上面的例子中,我们有两个中间件。为了让它们在任何一种情况下都能正常工作,我们需要编写它们以使它们能够适当地交互。
追求最大互操作性的中间件实现者可能需要考虑以下准则:
- 测试所需条件的请求。如果不满足该条件,请使用组合原型响应或组合响应工厂来生成并返回响应。
- 如果满足先决条件,则将响应的创建委托给提供的请求处理程序,可选地通过操纵提供的请求(例如,
$handler->handle($request->withAttribute('foo', 'bar'))来提供“新”请求。 - 要么原封不动地传递由请求处理程序返回的响应,要么通过操作返回的响应(例如,
return $response->withHeader('X-Foo-Bar', 'baz'))来提供新的响应。
它将执行所有这AuthorizationMiddleware三个准则:
- 如果需要授权,但请求未授权,它将使用组合原型响应来生成“未授权”响应。
- 如果不需要授权,它将把请求委托给处理程序而不做任何更改。
如果需要授权并且请求被授权,它会将请求委托给处理程序,并根据请求对返回的响应进行签名。
class AuthorizationMiddleware implements MiddlewareInterface { private $authorizationMap; public function __construct(AuthorizationMap $authorizationMap) { $this->authorizationMap = $authorizationMap; } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { if (! $this->authorizationMap->needsAuthorization($request)) { return $handler->handle($request); } if (! $this->authorizationMap->isAuthorized($request)) { return $this->authorizationMap->prepareUnauthorizedResponse(); } $response = $handler->handle($request); return $this->authorizationMap->signResponse($response, $request); } }请注意,中间件不关心请求处理程序是如何实现的;它只是在满足先决条件时使用它来产生响应。
下面RoutingMiddleware描述的实现遵循类似的过程:它分析请求以查看它是否与已知路由匹配。在这个特定的实现中,路由映射到请求处理程序,中间件本质上委托给它们以产生响应。但是,在没有匹配到路由的情况下,它将执行传递给它的处理程序以产生返回的响应。class RoutingMiddleware implements MiddlewareInterface { private $router; public function __construct(Router $router) { $this->router = $router; } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $result = $this->router->match($request); if ($result->isSuccess()) { return $result->getHandler()->handle($request); } return $handler->handle($request); } }PSR-17:HTTP 工厂
本文档描述了创建符合 PSR-7 的 HTTP 对象的工厂的通用标准。
PSR-7 没有包含关于如何创建 HTTP 对象的建议,这会导致需要在与 PSR-7 的特定实现无关的组件中创建新的 HTTP 对象时遇到困难。
本文档中概述的接口描述了可以实例化 PSR-7 对象的方法。
- 规范
HTTP 工厂是一种创建 PSR-7 定义的新 HTTP 对象的方法。HTTP 工厂必须为包提供的每种对象类型实现这些接口。
- 接口
以下接口可以在单个类或单独的类中一起实现。**RequestFactoryInterface**
具有创建客户端请求的能力。
<?php
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;
}
**ResponseFactoryInterface**
具有创建响应的能力。
<?php
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;
}
**ServerRequestFactoryInterface**
具有创建服务器请求的能力。
<?php
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;
}
**StreamFactoryInterface**
能够为请求和响应创建流。
<?php
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+');
**UploadedFileFactoryInterface**
能够为上传的文件创建流。
<?php
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;
}
**UriFactoryInterface**
能够为客户端和服务器请求创建 URI。
<?php
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-17:HTTP 工厂 - 说明文档
- 概述
此 PSR 的目的是提供定义创建 PSR-7 对象的方法的工厂接口。
- 为什么需要它?
PSR-7 的当前规范允许通过创建不可变副本来修改大多数对象。但是,有两个值得注意:
StreamInterface是基于资源的可变对象,仅当资源可写时才允许写入资源。UploadedFileInterface是基于不提供修改功能的资源的只读对象。
前者是 PSR-7 中间件的一个重要痛点,因为它会使响应处于不完整状态。如果附加到响应主体的流不可查找或不可写,则无法从主体已被写入的错误条件中恢复。
这种情况可以通过提供工厂来创建新流来避免。由于缺乏 HTTP 对象工厂的正式标准,开发人员必须依赖特定的供应商实现才能创建这些对象。
另一个痛点是在编写可重用的中间件或请求处理程序时。在这种情况下,包本身可能需要创建并返回响应。但是,创建离散实例会将包绑定到特定的 PSR-7 实现。如果这些包依赖于请求工厂接口,它们可以保持与 PSR-7 实现无关。
为工厂创建正式标准将允许开发人员避免对特定实现的依赖,同时能够在必要时创建新对象。
- 范围
目标
- 提供一组定义创建 PSR-7 兼容对象的方法的接口。
非目标
- 提供 PSR-7 工厂的具体实现。
- 方案
选择的方法
工厂方法定义是根据实例化后是否可以修改对象来选择的。对于无法修改的接口,必须在实例化时定义所有对象属性。UriInterface在一个完整的 URI 的情况下,为了方便可以传递。
使用的方法名称不会冲突。这允许单个类在适当的时候实现多个接口。
现有实现
PSR-7 的所有当前实现都定义了自己的要求。在大多数情况下,所需的参数与建议的工厂方法相同或不那么严格。
Diactoros
Diactoros 是最早用于服务器使用的 HTTP 消息实现之一,并且与 PSR-7 规范并行开发。
- Request 没有必需的参数,方法和 URI 默认为
null. - Response 不需要参数,状态码默认为
200. - ServerRequest 没有必需的参数。包含一个单独 ServerRequestFactory 用于从全局创建请求。
- Stream 对本身有要求
string|resource $stream。 - UploadedFile 需要
string|resource $streamOrFile,int $size,int $errorStatus. 错误状态必须是 PHP 上传常量。 - Uri 无必填参数,
string $uri默认为空。
总的来说,这种方法与提议的工厂非常相似。在某些情况下,Diactoros 提供了更多选项,这些选项对于有效对象来说不是必需的。建议的上传文件工厂允许大小和错误状态是可选的。
Guzzle
Guzzle 是一个专注于客户端使用的 HTTP 消息实现。
- Request 需要
string $method和string|UriInterface $uri。 - Response 不需要参数,状态码默认为
200. - Stream 对本身有要求
resource $stream。 - Uri 无必填参数,
string $uri默认为空。
Guzzle 面向客户端使用,不包含 ServerRequest或 UploadedFile实现。
总体而言,这种方法也与提议的工厂非常相似。一个显着的区别是 Guzzle 要求使用资源构建流并且不允许使用字符串。但是,它确实包含一个 stream_for 从内容字符串创建流的辅助函数和一个 try_fopen 从文件路径创建资源的函数。
Slim
Slim 是一个使用 3.0 版本以后的 HTTP 消息的微框架。
- Request 需要
string $method,UriInterface $uri,HeadersInterface $headers,array $cookies,array $serverParams和StreamInterface $body. 包含一个createFromEnvironment(Environment $environment)特定于框架但类似于提议的工厂方法。 - Response 不需要参数,状态码默认为
200. - Stream 对本身有要求
resource $stream。 - UploadedFile 需要
string $file源文件。 - Uri 需要
string $scheme和string $host。包含createFromString($uri)可用于Uri从字符串创建的工厂方法。
Slim 仅面向服务器使用,不包含Request. 上面列出的实现是ServerRequest.
在比较的方法中,Slim 与提议的工厂最不同。最值得注意的是,该Request实现包含特定于框架的要求,这些要求未在 HTTP 消息规范中定义。包含的工厂方法通常与建议的工厂相似。
潜在问题
建立这个标准最困难的任务是定义接口的方法签名。由于 PSR-7 中没有明确声明明确需要哪些值,因此必须根据接口是否具有复制和修改对象的方法来推断只读属性。
- 设计决策
为什么选择 PHP 7?
虽然 PSR-7 不针对 PHP 7,但本规范的作者指出,在撰写本文时(2018 年 4 月),PHP 5.6 已在 15 个月前停止接收错误修复,并且将在 8 个月后不再接收安全补丁;PHP 7.0 本身将在 7 个月内停止接收安全修复(有关当前支持的详细信息,请参阅PHP 支持的版本文档)。由于规范是长期的,因此作者认为规范应该针对在可预见的未来受支持的版本;PHP 5 不会。因此,从安全的角度来看,针对 PHP 7 下的任何内容都是对用户的伤害,因为这样做会默认使用不受支持的 PHP 版本。
此外,同样重要的是,PHP 7 使我们能够为我们定义的接口提供返回类型提示。这为最终用户保证了一个强大的、可预测的合约,因为他们可以假设实现返回的值正是他们所期望的。
为什么有多个接口?
每个提议的接口(主要)负责生成一种 PSR-7 类型。这允许消费者准确地输入他们需要的内容:如果他们需要响应,他们就输入提示ResponseFactoryInterface;如果他们需要 URI,他们会在UriFactoryInterface. 通过这种方式,用户可以细化他们的需求。
这样做还允许应用程序开发人员提供基于他们正在使用的 PSR-7 实现的匿名实现,仅生成特定上下文所需的实例。这减少了样板;开发人员不需要为未使用的方法编写存根。
为什么 ResponseFactoryInterface 的 $reasonPhrase 参数存在?ResponseFactoryInterface::createResponse()包括一个可选的 $reasonPhrase 字符串参数,在 PSR-7 规范中,您只能在提供状态码的同时提供原因短语,因为两者是相关的数据。本规范的作者选择模仿 PSR-7 ResponseInterface::withStatus()签名以确保两组数据都可能出现在创建的响应中。
为什么 ServerRequestFactoryInterface 的 $serverParams 参数存在?ServerRequestFactoryInterface::createServerRequest()包括一个可选的 $serverParams数组参数。提供它的原因是为了确保可以在填充了服务器参数的情况下创建实例。在通过 ServerRequestInterface可访问的数据中,唯一没有 mutator 方法的数据是对应于服务器参数的数据。因此,必须在初始创建时提供此数据。因此,它作为工厂方法的参数存在。
为什么没有工厂可以从超全局变量创建 ServerRequestInterface?ServerRequestInterface 的主要用例是从已知数据ServerRequestFactoryInterface创建新实例。围绕超全局数据编组的任何解决方案都假定:
- 超全局存在
- 超全局变量遵循特定的结构
这两个假设并不总是正确的。使用Swoole、ReactPHP等异步系统时:
- 不会填充标准超全局变量,例如
$_GET、$_POST、$_COOKIE和$_FILES - 不会填充
$_SERVER与标准 SAPI 相同的元素(例如 mod_php、mod-cgi 和 mod-fpm)
此外,不同的标准 SAPI 为$_SERVER 请求标头提供不同的信息和访问权限,需要不同的方法来初始填充请求。
因此,为超全局实例的填充设计一个接口超出了本规范的范围,并且应该在很大程度上是特定于实现的。
为什么 RequestFactoryInterface::createRequest 允许字符串 URI?RequestFactoryInterface::createRequest() 的主要用例RequestFactoryInterface是创建请求,任何请求的唯一必需值是请求方法和 URI。虽然可以接受一个UriInterface实例,但它也允许一个字符串。
理由是双重的。首先,大多数用例是创建请求实例;URI 实例的创建是次要的。需要一个UriInterface 手段,用户要么需要访问,要么对RequestFactoryInterface有硬性要求。第一个使工厂消费者的使用变得复杂,第二个使工厂的开发人员或创建工厂实例的人的使用变得复杂。
其次,UriFactoryInterface提供了一种创建 UriInterface实例的方法,即从字符串 URI。如果 URI 的创建基于字符串,则没有理由不允许使用相同的语义。此外,在开发此提案时调查的每个 PSR-7 实现都允许在创建 RequestInterface实例时使用字符串 URI,因为该值随后会传递给 UriInterface它们提供的任何实现。因此,接受一个字符串是方便的并且遵循现有的语义。
PSR-18:HTTP 客户端
本文档描述了用于发送 HTTP 请求和接收 HTTP 响应的通用接口。
- 概述
此 PSR 的目标是允许开发人员创建与 HTTP 客户端实现分离的库。这将使库更具可重用性,因为它减少了依赖项的数量并降低了版本冲突的可能性。
第二个目标是可以根据 里氏替换原则 替换 HTTP 客户端。这意味着所有客户端在发送请求时必须以相同的方式行事。
- 定义
- 客户端 - 客户端是一个实现此规范的库,目的是发送 PSR-7 兼容的 HTTP 请求消息并将 PSR-7 兼容的 HTTP 响应消息返回到调用库。
- 调用库 - 调用库是使用客户端的任何代码。它不实现本规范的接口,但使用实现它们的对象(客户端)。
Client
Client 是一个实现的对象ClientInterface。
客户端可以:
- 选择从提供的 HTTP 请求中发送更改后的 HTTP 请求。例如,它可以压缩传出的消息体。
- 选择在将接收到的 HTTP 响应返回到调用库之前对其进行更改。例如,它可以解压缩传入的消息体。
如果客户端选择更改 HTTP 请求或 HTTP 响应,它必须确保对象保持内部一致。例如,如果客户端选择解压缩消息体,那么它还必须删除Content-Encoding标头并调整Content-Length标头。
请注意,由于PSR-7 对象是不可变的,因此调用库不得假定传递给的ClientInterface::sendRequest()对象与实际发送的 PHP 对象相同。例如,异常返回的 Request 对象可能与传递给 sendRequest()的对象不同,因此无法通过引用 (===) 进行比较。
客户端必须:
- 重新组装多步 HTTP 1xx 响应本身,以便返回到调用库的是状态代码为 200 或更高的有效 HTTP 响应。
错误处理
客户端不得将格式正确的 HTTP 请求或 HTTP 响应视为错误条件。例如,400 和 500 范围内的响应状态代码不得导致异常,并且必须正常返回到调用库。
当且仅当客户端根本无法发送 HTTP 请求或 HTTP 响应无法解析为 PSR-7 响应对象时,客户端必须抛出一个 Psr\Http\Client\ClientExceptionInterface 实例。
如果由于请求消息不是格式正确的 HTTP 请求或缺少某些关键信息(例如主机或方法)而无法发送请求,则客户端必须抛出Psr\Http\Client\RequestExceptionInterface实例。
如果由于任何类型的网络故障(包括超时)而无法发送请求,客户端必须抛出Psr\Http\Client\NetworkExceptionInterface实例。
客户端可能会抛出比此处定义的更具体的异常(例如TimeOutException或HostNotFoundException),前提是它们实现了上面定义的适当接口。
- 接口
ClientInterface
<?php
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;
}
ClientExceptionInterface
<?php
namespace Psr\Http\Client;
/**
* 每个 HTTP 客户端相关的异常都必须实现此接口
*/
interface ClientExceptionInterface extends \Throwable
{
}
RequestExceptionInterface
<?php
namespace Psr\Http\Client;
use Psr\Http\Message\RequestInterface;
/**
* 请求失败时的异常
*
* 例如:
* - 请求无效 (e.g. 参数缺失)
* - 请求运行错误 (e.g. 响应体不可见)
*/
interface RequestExceptionInterface extends ClientExceptionInterface
{
/**
* 获取请求对象
*
* 请求对象可能和客户端接口发送的对象不一致
*
* @return RequestInterface
*/
public function getRequest(): RequestInterface;
}
NetworkExceptionInterface
<?php
namespace Psr\Http\Client;
use Psr\Http\Message\RequestInterface;
/**
* 因网络原因导致请求无法完成时抛出该异常
*
* 抛出该异常将没有响应体,因为收不到响应体时也会抛出这个异常
*
* 例如:域名不能解析或连接失败
*/
interface NetworkExceptionInterface extends ClientExceptionInterface
{
/**
* 返回请求对象
*
* 返回的请求对象可能和客户端接口发送的对象不一致
*
* @return RequestInterface
*/
public function getRequest(): RequestInterface;
}
PSR-18:HTTP 客户端 - 说明文档
- 概述
HTTP 请求和响应是 Web 编程中的两个基本对象。所有与外部 API 通信的客户端都使用某种形式的 HTTP 客户端。许多库耦合到一个特定的客户端或自己实现客户端或适配器层。这会导致糟糕的库设计、版本冲突或与库域无关的代码。
- 为什么需要它?
多亏了 PSR-7,我们才知道 HTTP 请求和响应的理想情况,但没有定义应该如何发送请求和接收响应。HTTP 客户端的通用接口将允许库与特定实现分离。
- 范围
目标
- 用于发送 PSR-7 消息和返回 PSR-7 响应的通用接口。
非目标
- 对异步 HTTP 请求的支持留待未来的另一个 PSR。
- 此 PSR 未定义如何配置 HTTP 客户端。它仅指定默认行为。
- 此 PSR 对中间件的使用持中立态度。
异步 HTTP 客户端
此 PSR 未涵盖异步请求的原因是缺少 Promises 的通用标准。Promise 足够复杂,它们应该有自己的规范,不应该包含在这个规范中。
一旦接受了 Promise PSR,就可以在单独的 PSR 中定义异步请求的单独接口。异步请求的方法签名必须不同于同步请求的方法签名,因为异步调用的返回类型将是 Promise。因此,这个 PSR 是前向兼容的,客户端将能够根据他们期望公开的特性实现一个或两个接口。
- 方案
默认行为
此 PSR 的目的是为库开发人员提供具有良好定义行为的 HTTP 客户端。库应该能够使用任何兼容的客户端而无需特殊代码来处理客户端实现细节(里氏替换原则)。PSR 不会尝试限制或定义如何配置 HTTP 客户端。
另一种方法是将配置传递给客户端。这种方法会有一些缺点:
- 配置必须由 PSR 定义。
- 所有客户端都必须支持定义的配置。
- 如果没有配置传递给客户端,则行为是不可预测的。
命名原理
主要界面行为由方法定义sendRequest(RequestInterface $request): ResponseInterface。
虽然已经提出了更短的方法名称send(),但它已经被现有的和非常常见的 HTTP 客户端(如 Guzzle)使用。因此,如果他们要采用这个标准,他们可能需要打破向后兼容性才能实现该规范。相反,通过定义sendRequest(),我们确保他们可以在没有任何立即中断的情况下采用。
异常模型
异常NetworkExceptionInterface和RequestExceptionInterface的定义非常相似。选择的方法是不要让它们相互扩展,因为继承在域模型中没有意义。RequestExceptionInterface根本不是 NetworkExceptionInterface。
已经讨论了允许异常扩展一个RequestAwareException或接口,但这是一个不应该采用的方便快捷方式。人们应该捕获特定的异常并相应地处理它们。
在定义异常时可能会更细化。例如,TimeOutException和HostNotFoundException 可能是 NetworkExceptionInterface的子类型。选择的方法不是定义这样的子类型,因为在大多数情况下,消费库中的异常处理在这些异常之间不会有所不同。
为 4xx 和 5xx 响应抛出异常
最初的想法是允许客户端配置为对 HTTP 状态为 4xx 和 5xx 的响应抛出异常。这种方法是不可取的,因为使用库必须两次检查 4xx 和 5xx 响应:首先,通过验证响应上的状态代码,然后通过捕获潜在异常。
为了使规范更具可预测性,决定 HTTP 客户端永远不会为 4xx 和 5xx 响应抛出异常。
中间件和封装客户端
该规范对想要包装/装饰 HTTP 客户端的中间件或类没有任何限制。如果装饰类也实现了ClientInterface ,那么它也必须遵循规范。
允许配置或向 HTTP 客户端添加中间件是很容易的,这样它就可以跟随重定向或抛出异常。如果这是应用程序开发人员的决定,他们已经明确表示他们想要打破规范。这是应用程序开发人员应该处理的问题(或功能)。第三方库不得假定 HTTP 客户端违反了规范。
背景
HTTP 客户端 PSR 由php-http 团队启发和创建。早在 2015 年,他们就创建了 HTTPlug 作为 HTTP 客户端的通用接口。他们想要第三方库可以使用的抽象,以便不依赖特定的 HTTP 客户端实现。2016 年 1 月标记了一个稳定版本,从那时起该项目已被广泛采用。在最初的稳定版本发布后的两年内,下载量超过 300 万次,是时候将这个“事实上的”标准转换为正式规范了。
编码风格
PSR-1:基本编码规范
该标准的这一部分包括应被视为确保共享 PHP 代码之间的高水平技术互操作性所需的标准编码元素。
- 概述
- 文件必须使用
<?php和<?=标签。 - 对于 PHP 代码,文件必须使用不带 BOM 的 UTF-8。
- 文件应该声明标志(类、函数、常量等) 或引起副作用(例如生成输出、更改 .ini 设置等),但不应该两者都做。
- 命名空间和类必须遵循“自动加载” PSR:[ PSR-0 , PSR-4 ]。
- 类名必须遵循
StudlyCaps. - 类常量必须用下划线分隔符全部大写。
- 方法名称必须遵循
camelCase.
- 文件
PHP 标签
PHP 代码必须使用长<?php ?>标签或短<?= ?>标签;它不得使用其他标签。
字符编码
PHP 代码必须使用没有 BOM 的 UTF-8。
副作用
一个文件应该声明新的标志(类、函数、常量等)并且不会引起其他副作用,或者它应该执行具有副作用的逻辑,但不应该两者都做。
短语“副作用”是指执行与声明类、函数、常量等没有直接关系的逻辑,仅来自包含文件。
“副作用”包括但不限于:生成输出、显式使用requireor include、连接到外部服务、修改 ini 设置、发出错误或异常、修改全局或静态变量、读取或写入文件等。
以下是一个同时具有声明和副作用的文件的示例;即应避免的示例:
<?php
// 副作用: 修改 ini 配置
ini_set('error_reporting', E_ALL);
// 副作用:引入文件
include "file.php";
// 副作用:生成输出
echo "<html>\n";
// 声明函数
function foo()
{
// function body
}
以下示例是一个包含没有副作用的声明文件;即要模拟的示例:
<?php
// 声明函数
function foo()
{
// function body
}
// 条件声明不属于副作用
if (! function_exists('bar')) {
function bar()
{
// function body
}
}
- 命名空间和类名
命名空间和类必须遵循“自动加载” PSR:[ PSR-0 , PSR-4 ]。
这意味着每个类都在一个文件中,并且在至少一个级别的命名空间中:顶级供应商名称。
类名必须遵循StudlyCaps.
为 PHP 5.3 及之后编写的代码必须使用正式的命名空间。
例如:
<?php
// PHP 5.3 及更高版本:
namespace Vendor\Model;
class Foo
{
}
Vendor_为 5.2.x 和之前编写的代码应该使用类名前缀的伪命名空间约定。
<?php
// PHP 5.2.x 及更低版本:
class Vendor_Model_Foo
{
}
- 类的常量、属性和方法
术语“类”指的是所有类、接口和特征。
常量
类常量必须用下划线分隔符全部大写。例如:
<?php
namespace Vendor\Model;
class Foo
{
const VERSION = '1.0';
const DATE_APPROVED = '2012-06-01';
}
属性
本指南有意避免任何有关使用 $StudlyCaps、$camelCase或$under_score属性名称的建议。
无论使用什么命名约定,都应该在合理的范围内一致地应用。该范围可能是供应商级别、包级别、类级别或方法级别。
方法
方法名称必须遵循camelCase().
PSR-12:扩充编码规范
- 概述
该规范起到继承,扩展和替换了PSR-2,编码风格指南,并要求遵守基本编码标准PSR-1。
与PSR-2一样,该规范的目的是减少不同人阅读代码时的认知摩擦。它通过枚举一组关于如何格式化 PHP 代码的共享规则和期望来做到这一点。此 PSR 旨在提供一套编码风格工具可以实现的方式,项目可以声明遵守,开发人员可以轻松地在不同项目之间建立联系。当不同的人跨多个项目进行协作时,在所有这些项目中使用一套指南会很有帮助。因此,本指南的好处不在于规则本身,而在于共享这些规则。
PSR-2于 2012 年被接受,此后对 PHP 进行了许多更改,这对编码风格指南有影响。虽然PSR-2非常全面地包含了撰写本文时存在的 PHP 功能,但新功能的解释非常开放。因此,本 PSR 试图在更现代的环境中阐明 PSR-2 的内容,并提供新的功能,并使勘误表与 PSR-2 绑定。
以前的语言版本
在本文档中,如果您的项目支持的 PHP 版本中不存在任何说明,则可以忽略它们。
Example
此示例包含以下一些规则作为快速概述:
<?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()
{
// method body
}
}
- 通用
基本编码标准
代码必须遵循PSR-1中列出的所有规则。
PSR-1 中的术语“StudlyCaps”必须被解释为 PascalCase,其中每个单词的第一个字母都大写,包括第一个字母。
文件
所有 PHP 文件必须使用 Unix LF(换行)行结尾。
所有 PHP 文件必须以非空行结束,并以单个 LF 结束。?>必须从包含 PHP 的文件中省略结束标记。
代码行
行长不能有硬性限制。
行长度的软限制必须是 120 个字符。
行不应超过 80 个字符;超过这个长度的行应该分成多个后续行,每行不超过 80 个字符。
行尾不得有尾随空格。
可以添加空行以提高可读性并指示相关的代码块,除非明确禁止。
每行不得有超过一个语句。
缩进
代码必须为每个缩进级别使用 4 个空格的缩进,并且不得使用制表符进行缩进。
关键字和类型
所有 PHP 保留关键字和类型[1][2]必须小写。
添加到未来 PHP 版本的任何新类型和关键字都必须小写。
必须使用类型关键字的缩写形式,即bool代替boolean, int代替integer等。
- 声明语句、命名空间和导入语句
PHP 文件的标头可能由许多不同的块组成。如果存在,下面的每个块必须由一个空行分隔,并且不得包含空行。每个块必须按照下面列出的顺序排列,尽管可以省略不相关的块。
<?php开始标签。- 文件级文档块。
- 一个或多个声明语句。
- 文件的命名空间声明。
- 一个或多个基于类的
use导入语句。 - 一个或多个基于函数的
use导入语句。 - 一个或多个基于常量的
use导入语句。 - 文件中的其余代码。
当文件包含 HTML 和 PHP 的混合时,仍可以使用上述任何部分。如果是这样,它们必须出现在文件的顶部,即使代码的其余部分包含一个结束 PHP 标记,然后是 HTML 和 PHP 的混合。
当开始<?php标签在文件的第一行时,它必须在自己的行上,没有其他语句,除非它是包含 PHP 开始和结束标签之外的标记的文件。
导入语句绝不能以反斜杠开头,因为它们必须始终是完全限定的。
以下示例说明了所有块的完整列表:
<?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
{
// ... additional PHP code ...
}
不得使用深度超过 2 的复合命名空间。因此,以下是允许的最大复合深度:
<?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
// ... additional PHP code ...
?>
</body>
</html>
Declare 语句必须不包含空格并且必须完全正确declare(strict_types=1) (带有可选的分号终止符)。
块声明语句是允许的,并且必须按如下格式设置。注意大括号的位置和间距:
declare(ticks=1) {
// some code
}
- 类、属性和方法
术语“类”指的是所有类、接口和特征。
任何右大括号后不得在同一行上跟任何注释或语句。
实例化新类时,即使没有传递给构造函数的参数,括号也必须始终存在。
new Foo();
继承和实现
extends 和 implements 关键字必须与类名在同一行声明。
类的左大括号必须单独一行;类的右大括号必须在正文之后的下一行。
左大括号必须在自己的行上,并且不得在其前后有空行。
右大括号必须在自己的行上,并且不能在前面有空行。
在接口的情况下,extends 和 implements 可以分成多行,其中每个后续行缩进一次。这样做时,列表中的第一项必须在下一行,并且每行必须只有一个接口。
<?php
namespace Vendor\Package;
use FooClass;
use BarClass as Bar;
use OtherVendor\OtherPackage\BazClass;
class ClassName extends ParentClass implements
\ArrayAccess,
\Countable,
\Serializable
{
// constants, properties, methods
}
使用特征
在类中用于实现特征的 use 关键字必须在左大括号后的下一行声明。
<?php
namespace Vendor\Package;
use Vendor\Package\FirstTrait;
class ClassName
{
use FirstTrait;
}
导入到类中的每个单独的特征必须每行包含一个,并且每个包含必须有自己的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;
}
当类在useimport 语句之后没有任何内容时,类右大括号必须在useimport 语句之后的下一行。
<?php
namespace Vendor\Package;
use Vendor\Package\FirstTrait;
class ClassName
{
use FirstTrait;
}
否则,它必须在useimport 语句之后有一个空行。
<?php
namespace Vendor\Package;
use Vendor\Package\FirstTrait;
class ClassName
{
use FirstTrait;
private $property;
}
使用insteadof和as运算符时,它们必须按如下方式使用,注意缩进、间距和换行。
<?php
class Talker
{
use A;
use B {
A::smallTalk insteadof B;
}
use C {
B::bigTalk insteadof C;
C::mediumTalk as FooBar;
}
}
属性和常量
必须在所有属性上声明可见性。
如果您的项目 PHP 最低版本支持常量可见性(PHP 7.1 或更高版本),则必须在所有常量上声明可见性。var关键字不得用于声明属性。
每个语句声明的属性不得超过一个。
属性名称不能以单个下划线作为前缀来表示受保护或私有可见性。也就是说,下划线前缀明确没有意义。
类型声明和属性名称之间必须有空格。
属性声明如下所示:
<?php
namespace Vendor\Package;
class ClassName
{
public $foo = null;
public static int $bar = 0;
}
4.4 方法和函数
必须在所有方法上声明可见性。
方法名称不得以单个下划线作为前缀来表示受保护或私有可见性。也就是说,下划线前缀明确没有意义。
方法名和函数名不得在方法名后用空格声明。左大括号必须单独一行,右大括号必须在正文之后的下一行。左括号后不得有空格,右括号前不得有空格。
方法声明如下所示。注意括号、逗号、空格和大括号的位置:
<?php
namespace Vendor\Package;
class ClassName
{
public function fooBarBaz($arg1, &$arg2, $arg3 = [])
{
// method body
}
}
函数声明如下所示。注意括号、逗号、空格和大括号的位置:
<?php
function fooBarBaz($arg1, &$arg2, $arg3 = [])
{
// function body
}
4.5 方法和函数参数
在参数列表中,每个逗号前不得有空格,每个逗号后必须有一个空格。
具有默认值的方法和函数参数必须放在参数列表的末尾。
<?php
namespace Vendor\Package;
class ClassName
{
public function foo(int $arg1, &$arg2, $arg3 = [])
{
// method body
}
}
参数列表可以拆分为多行,其中每个后续行缩进一次。这样做时,列表中的第一项必须在下一行,并且每行必须只有一个参数。
当参数列表被分成多行时,右括号和左大括号必须一起放在各自的行上,它们之间有一个空格。
<?php
namespace Vendor\Package;
class ClassName
{
public function aVeryLongMethodName(
ClassTypeHint $arg1,
&$arg2,
array $arg3 = []
) {
// method body
}
}
当您有返回类型声明时,冒号后面必须有一个空格,然后是类型声明。冒号和声明必须与参数列表右括号位于同一行,两个字符之间没有空格。
<?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)
{
// processing
}
当结合引用运算符和可变三点运算符时,它们两者之间不得有任何空格:
public function process(string $algorithm, &...$parts)
{
// processing
}
4.6 abstract、final和static
如果存在,abstractandfinal声明必须在可见性声明之前。
如果存在,static声明必须在可见性声明之后。
<?php
namespace Vendor\Package;
abstract class ClassName
{
protected static $foo;
abstract protected function zim();
final public static function bar()
{
// method body
}
}
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.1 if, elseif, elseif结构如下所示。注意括号、空格和大括号的位置;和那个else和elseif与前面主体的右大括号在同一行。
<?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, caseswitch结构如下所示。注意括号、空格和大括号的位置。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 whilewhile语句如下所示。注意括号、空格和大括号的位置。
<?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 forfor语句如下所示。注意括号、空格和大括号的位置。
<?php
for ($i = 0; $i < 10; $i++) {
// for body
}
括号中的表达式可以分成多行,其中每个后续行至少缩进一次。这样做时,第一个表达式必须在下一行。右括号和左大括号必须放在各自的行上,它们之间有一个空格。
<?php
for (
$i = 0;
$i < 10;
$i++
) {
// for body
}
5.5 foreachforeach语句如下所示。注意括号、空格和大括号的位置。
<?php
foreach ($iterable as $key => $value) {
// foreach body
}
5.6 try, catch, finally
一个try-catch-finally块如下所示。注意括号、空格和大括号的位置。
<?php
try {
// try body
} catch (FirstThrowableType $e) {
// catch body
} catch (OtherThrowableType | AnotherThrowableType $e) {
// catch body
} finally {
// finally body
}
- 运算符
运算符的样式规则按 arity(它们采用的操作数的数量)分组。
当操作符周围允许有空格时,为了便于阅读,可以使用多个空格。
此处未描述的所有运算符均未定义。
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. 三元运算符? 条件运算符,也简称为三元运算符,必须在and:字符前后至少有一个空格:
$variable = $foo ? 'foo' : 'bar';
当条件运算符的中间操作数被省略时,该运算符必须遵循与其他二元比较运算符相同的样式规则:
$variable = $foo ?: 'bar';
- 闭包
闭包声明必须在function关键字后加一个空格,关键字前后各加一个空格use。
左大括号必须在同一行,右大括号必须在正文之后的下一行。
参数列表或变量列表的左括号后不得有空格,参数列表或变量列表的右括号前不得有空格。
在参数列表和变量列表中,每个逗号前不得有空格,每个逗号后必须有一个空格。
具有默认值的闭包参数必须放在参数列表的末尾。
如果存在返回类型,它必须遵循与普通函数和方法相同的规则;如果use关键字存在,冒号必须跟在use列表右括号之后,两个字符之间没有空格。
闭包声明如下所示。注意括号、逗号、空格和大括号的位置:
<?php
$closureWithArgs = function ($arg1, $arg2) {
// body
};
$closureWithArgsAndVars = function ($arg1, $arg2) use ($var1, $var2) {
// body
};
$closureWithArgsVarsAndReturn = function ($arg1, $arg2) use ($var1, $var2): bool {
// body
};
参数列表和变量列表可以分成多行,其中每个后续行缩进一次。这样做时,列表中的第一项必须在下一行,并且每行必须只有一个参数或变量。
当结束列表(无论是参数还是变量)被分成多行时,右括号和左大括号必须放在各自的行上,它们之间有一个空格。
以下是带有和不带有参数列表的闭包示例,以及拆分为多行的变量列表。
<?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
);
- 匿名类
匿名类必须遵循与上一节中的闭包相同的准则和原则。
<?php
$instance = new class {};
class只要implements接口列表不换行,左大括号可以与关键字在同一行。如果接口列表换行,大括号必须放在紧跟最后一个接口的行上。
<?php
// Brace on the same line
$instance = new class extends \Foo implements \HandleableInterface {
// Class content
};
// Brace on the next line
$instance = new class extends \Foo implements
\ArrayAccess,
\Countable,
\Serializable
{
// Class content
};
PSR-12:扩充编码规范 - 说明文档
本文档描述了导致扩展编码风格 PSR 的过程和讨论。它的目标是解释每个决定背后的原因。
PSR-2 于 2012 年被接受,此后对 PHP 进行了许多更改,最值得注意的是最近对 PHP 7 的更改,这些更改对编码风格指南有影响。虽然 PSR-2 非常全面地包含了撰写本文时存在的 PHP 功能,但新功能的解释非常开放。PSR-12 旨在提供一种既可以实现编码风格工具又可以实现项目的固定方式,项目可以声明遵守,并且开发人员可以轻松地在不同项目之间建立这些编码风格,从而减少认知摩擦。
PSR-2 是根据当时 PHP-FIG 项目的通用实践创建的,但最终这意味着它是许多不同项目指南的折衷方案。项目更改其编码指南以符合 PSR-2(几乎所有项目都符合 PSR-1,即使没有明确说明)的影响被认为太大(丢失 git 历史、巨大的变更集和破坏现有的补丁) /拉请求)。
PSR-2 要求采用者重新格式化大量阻碍采用的现有代码。为了帮助缓解 PSR-12 的这个问题,我们采用了更具规范性的方法,并在新语言功能发布时定义了标准。
然而,由于不想独裁,我们的目标是在 PSR-12 中应用 PSR-2 样式、基本原理和立场(在第 4 节,方法中描述),而不是建立新的约定。
- 范围
目标
此 PSR 与 PSR-2 具有相同的目标。
本指南的目的是减少扫描来自不同作者的代码时的认知摩擦。它通过枚举一组关于如何格式化 PHP 代码的共享规则和期望来做到这一点。当不同的作者跨多个项目进行协作时,在所有这些项目中使用一套指南会很有帮助。因此,本指南的好处不在于规则本身,而在于共享这些规则。
该 PSR 是 PSR-2 的扩展,因此也是 PSR-1 的扩展。PSR-12 的基础是 PSR-2,因此下面提供了差异列表以帮助迁移,但应将其视为独立规范。
此 PSR 将包括与 PSR-2 发布后添加到 PHP 的新功能相关的编码风格指南;这包括 PHP 5.5、PHP 5.6 和 PHP 7.0。本 PSR 还将包括对 PSR-2 文本的澄清,如 PSR-2 勘误表中所述。
非目标
本 PSR 无意添加全新的编码风格指南。PSR-12 也不会改变 PSR-1 和 PSR-2 中规定的任何内容。
总体方法是尝试将现有的 PSR-2 样式和基本原理应用于新功能,而不是建立新的约定。
- 类型声明
4.1。严格类型声明
关于是否应在标准 https://github.com/cs-extended/fig-standards/issues/7 中强制执行严格类型的讨论。所有人都同意我们应该只使用 MUST 或 MUST NOT 语句并避免使用 SHOULD 语句,并且没有人想说不能声明严格类型。讨论是是否应将其视为应涵盖的编码风格项目,或者是否超出范围并决定超出编码风格指南的范围。
4.2. 最后和返回类型声明间距
建议了许多不同的选项,可以在 此处查看返回类型声明或 此处用于 finally 块 ,并且选择当前实现是因为与来自 PSR-2 的 PSR-12 规范的其他部分保持一致。
4.3. 对所有类型的关键字强制执行简写形式
PHP 7.0 引入了 不支持长类型别名的标量类型声明。因此,强制使用主要的短类型形式以具有统一的语法并防止可能的混淆是有意义的。
4.5. 多行函数参数与多行返回混合
邮件列表中提出了一个潜在的可读性问题。我们审查了可以提供更好可读性的规范更改选项,浮动选项是如果参数和返回都是多行的,则在函数的左括号后需要一个空行。相反,有人指出,该规范 已经允许您决定要在哪里添加空行,因此我们将把它留给实现者来决定。
请注意,此变更日志不是 PSR-2 变更的详细列表,而是突出显示了最显着的变更。它应该被视为一个新规范,因此您应该阅读该规范以全面了解其内容。
- 新声明
- 所有关键字的小写字母 - 第 2.5 节
- 所有类型关键字的简写形式 - 第 2.5 节
- 使用语句分组 - 第 3 节
- 使用语句块 - 第 3 节
- 声明语句/严格类型声明用法 - 第 3 节
- 类实例化始终需要括号 - 第 4 节
- 类型化属性 - 第 4.3 节
- 返回类型声明 - 第 4.5 节
- 可变参数和引用参数运算符 - 第 4.5 节
- 类型提示 - 第 4.5 节
- 添加 finally 块 - 第 5.6 节
- 运营商 - 第 6 节
- 一元运算符 - 第 6.1 节
- 二元运算符 - 第 6.2 节
- 三元运算符 - 第 6.3 节
- 匿名类 - 第 8 节
- 澄清和勘误
- 在许多情况下将“方法”调整为“方法和功能” - 贯穿始终
- 调整对类和接口的引用以包括特征 - 贯穿始终
- StudlyCaps 的含义被澄清为 PascalCase - 第 2.1 节
- 最后一行不应为空,但应包含 EOL 字符 - 第 2.2 节
- 可以添加空行以提高可读性,除非 PSR 中明确禁止 - 第 2.3 节
- PSR-2 关于多行参数的勘误声明 - 第 4 节
- PSR-2 关于扩展多个接口的勘误表 - 第 4 节
- 禁止在类的关闭/打开大括号之前/之后使用空行 - 第 4 节
