ryunosuke/stream-wrapper

v1.1.2 2024-08-09 11:51 UTC

This package is auto-updated.

Last update: 2024-09-09 12:05:06 UTC


README

描述

这是一个用于规范php流包装器的包。

php的流包装器功能强大且灵活,但使用起来困难,即使阅读了文档,也很难理解其实际实现方法。此外,实现时接口被切断,需要一边阅读文档一边在尝试中不断尝试错误地实现。另外,有些方法(看似没有关系)实际上使用相同的参数分支,依赖关系非常难以理解。

因此,该包的主要目的是预先准备一个实现接口和 trait,以使接口声明和预定义的接口和 trait 能够作为流包装器运行。

安装

{
  "require": {
    "ryunosuke/stream-wrapper": "dev-master"
  }
}

功能

目录结构

├── Exception/
├── Utils/
├── Stream/
├── Mixin
│   ├── DelegateTrait.php
│   ├── DirectoryIOTrait.php
│   ├── DirectoryIteratorTrait.php
│   ├── StreamTrait.php
│   ├── UrlIOTrait.php
│   └── UrlPermissionTrait.php
├── PhpStreamWrapperInterface.php
├── StreamWrapperAdapterInterface.php
├── StreamWrapperAdapterTrait.php
└── StreamWrapperNoopTrait.php

顶级目录

这就是这个包的目标。其他都是赠品

PhpStreamWrapperInterface

这是从php原始流包装器中提取的 interface。

可能是因为php本身没有提供接口,我认为其中一部分原因可能是有些方法不应该被实现。例如,mkdir( https://php.ac.cn/manual/streamwrapper.mkdir.php )等,“如果包装器不支持目录创建,则不应定义此方法以返回适当的错误消息”。还有其他一些方法也有类似的说明。如果相信这些话,那么“为了返回适当的错误消息”只是一个程度的问题,因为接口不能为了错误消息而切断,这非常不方便。因此,在这个包中,我们决定以例外的方式处理接口。

实际上,这并不需要。由于本家没有提供接口,即使不使用也不会导致错误。为了将运行时错误转换为编译时错误,主要是在开发时准备的。

StreamWrapperAdapterInterface

这是本包中用于规范流包装器的 interface。

正如前面所述,标准流包装器的命名难以理解,或者有奇怪的情况划分,或者突然出现 context 这样的概念(并且是资源),所以这个 interface 切割了一个“函数名和接口名一致”的接口,并通过 StreamWrapperAdapterTrait 将其连接到本家。这样,context 或情况划分等都被吸收,形成了简洁的方法集和对应的函数映射结构。

StreamWrapperAdapterTrait

这是一个连接 StreamWrapperAdapterInterface 和 PhpStreamWrapperInterface 的 trait。

StreamWrapperAdapterInterface 仅仅是定义了一个接口规范,并不能作为流包装器运行。要使其运行,需要 PhpStreamWrapperInterface 的实现以及连接它们的样板代码。这就是这个 trait 承担的角色。

很少使用联合类型作为返回值(目前只有 readdir 是特例)。如果无法返回适当的值,则抛出 ErrorException,以便将其转换为 php 错误。

context 这个概念被取消了。context 被解析,并作为参数数组传递。context 中还有 options 和 params 这样的概念,所以它接受两个参数。然而,关于 params 的文档非常少,几乎没有人使用它(官方文档中只有一个 stream_notification_callback 的例子)。

StreamWrapperNoopTrait

实现了 StreamWrapperAdapterInterface 的所有方法,但都抛出异常的 trait。

在实现流包装器时,需要实现所有不使用的方法,因此这个 trait 出现了补充作用,它在所有方法中都抛出异常(有少数特例)。通过使用这个 trait,在实现类中只需要实现所需的方法,该类就可以作为流包装器运行。

class HogeStream implements
    PhpStreamWrapperInterface,    // これにより、php オリジナルのストリームラッパーインターフェースがすべて規約されます(なくても動きますが、エラーが実行時になります)
    StreamWrapperAdapterInterface // これにより、オレオレストリームラッパーインターフェースがすべて規約されます
{
    use StreamWrapperAdapterTrait; // オレオレストリームラッパーと php オリジナルのストリームラッパーを接続させるための trait です
    use StreamWrapperNoopTrait;    // すべてが例外を投げるデフォルト実装 trait です

    public function _stat(string $url): array // これを実装すれば stat(filesize や filemtime) を実装したことになります
    {
        // ...
    }
}

以下是方法的详细信息。其中一些进行了修改。阅读完毕后,您可能会对流包装器的混乱状态有更深的理解。

_mkdir

函数版本参数分开,但流版本以位标志的形式传递,因此我们将其分开调用。也就是说,接口版本 mkdir 和函数版本相同(context 除外)。

_rmdir

实现时应实现的流版本有一个神秘的参数 $options,但我们没有实现它。文档中存在,“STREAM_MKDIR_RECURSIVE 等值的位掩码”,但没有这样的参数在函数版本中。确实,如果 rmdir 支持递归删除,那么这将是很有用的...我认为这是 mkdir 的复制粘贴错误。

_touch

没有存在与函数版 tocuh 对应的流版本方法。在 stream_metadata 的参数分支中实现了。详细说明了“如果是 tocuh 的话...如果是 chmod 的话...”,接口和 trait 已经全部吸收了。也就是说,接口版本 tocuh 几乎与函数版本相同。“几乎”是因为 ?int 变成了 int。省略时或同时指定时的值都解决了,并传递过去了。

顺便提一下,命名规则有问题,参数是路径,所以我认为 stream_metadata 应该用 url_metadata 才正确。(大概是“因为是流包装器的函数”这个理由吧。但这样的话,utl_stat 的说明就不够了)。

_chmod

没有与 chmod 函数对应的流函数。在 stream_metadata 的参数分叉中实现了。也就是说,在所有情况下都与 touch 的说明相同。

_chown

没有与 chown 函数对应的流函数。在 stream_metadata 的参数分叉中实现了。也就是说,在所有情况下都与 touch 的说明相同,但在调用时会使用变量名,所以 $uid 是 int。

_chgrp

没有与 chgrp 函数对应的流函数。在 stream_metadata 的参数分叉中实现了。也就是说,在所有情况下都与 touch 的说明相同,但在调用时会使用变量名,所以 $gid 是 int。

_unlink

没有什么变化。

_rename

没有什么变化。

_stat

没有什么变化。另外,由于是 stat,所以没有办法,这个方法对应了很多函数。在大多数自定义包装器中,实现是必需的(至少返回 size 也很好)。

_lstat

没有对应的流方法。由于链接这个概念受文件系统的影响很大,所以作为流包装器调用的可能性几乎不存在。因此,在本包中,为了与函数一对一对应,特意分开了。因此,这个方法被特别处理,调用时会委派给默认的 _stat 而不是“未实现异常”。

_opendir

有应该实现的流版本中有一个神秘的参数 $options,但还没有实现。因为文档不存在,所以也不清楚这个参数是为了什么。最初以为它会对应 scandir 的 $sorting_order 参数,但似乎并不是这样。

由于这个 opendir 和 fopen 是打开流的函数,所以需要内部状态。本包不喜欢持有状态,所以用参数传递。也就是说,如果 _opendir 返回一个对象,那么这个对象将作为 _readdir, _closedir 的参数传递。通过这种规格,不需要保持打开的任何东西,例如属性。使用时也是因为“用 fopen 打开的资源传递给 fread 等使用”的用法,所以我认为这种规格更直观。在直接实现 StreamWrapper 时,“fread 没有参数,但是如何获取 fopen 的资源?”这样的情况很常见。

_readdir

$dir_handle 的省略是不必要的,因此还没有实现。PHP 常见的是“参数是可选的,省略时使用最后一个资源”,这是过去的规定,现在几乎不再需要。

另外,原始值是 string|false(*文档是 string,但可能是错误*),如果完成,则返回 null。这仅是因为它可以表示为 ?string 的原因。如果 PHP8 移行后可以使用联合类型,可能会将其改为 array|false(个人原因),但这将是很久以后的事情。

_rewinddir

没有什么变化。

_closedir

没有什么变化。

_fopen

存在一些神秘的参数,但它们都被吸收了,因此从接口上看,它与 fopen 几乎相同。关于返回值,可以说与 _opendir 相同,这个 _fopen 返回的对象将被用作 _fread_fwrite 等方法的参数。

以下是修改部分。

首先,$options,正如其名所示,对应于 STREAM_USE_PATH。它对应于 foepnfile_get_contents 的第三个参数。…但在这个包中几乎没有对应,也没有传递参数。这是因为,由于自定义流包装器的特性,$path 是所有从方案开始的完整路径,不会传入相对路径。此外,包含路径是基于文件方案考虑的,而自定义流的相对路径被设置为包含路径的情况几乎不存在。(在非常有限的情况下,fopen 无方案调用是存在的,因此 STREAM_USE_PATH 可能是为此功能而设计的,但在这里我们省略了它)。

接下来是 STREAM_REPORT_ERRORS,但几乎找不到它被传递的情况。尽管如此,我们还是提供了一定的支持,并且当此标志未设置时,警告将被抑制(因此 fopen 的错误会报告,但无法获得详细的错误信息)。在检查 php-src 时,发现内部使用了 REPORT_ERRORS,似乎它在某些情况下被抑制,例如在读取文件时不是文件打开的主要目的时。例如 finfo 或 exif 等。这非常不方便,因此可能会进行修正以始终输出错误。

$opened_path 几乎没有实现。这是从包含路径中找到的完整路径的接收参数,但正如上面所述,这种情况几乎不存在。

_fread

没有特别的变化。如果敢说的话,文档中有许多注意事项需要注意。

  • 如果返回值比 count 长的话,将发生 E_WARNING 错误,并且将丢失额外的数据。
  • streamWrapper::stream_eof() 在 streamWrapper::stream_read() 被调用后直接调用,以检查是否已达到 EOF。如果没有实现,则视为 EOF。
  • 当使用 (file_get_contents() 等) 读取整个文件时,PHP 会先调用 streamWrapper::stream_read(),然后调用 streamWrapper::stream_eof()。但是,只要 streamWrapper::stream_read() 返回非空字符串,就会忽略 streamWrapper::stream_eof() 的返回值。

这是 PHP 的规范,因此不是包可以解决的问题,所以我们只引用它。

_fwrite

没有什么变化。

_ftruncate

没有什么变化。

_fclose

没有特别的变化。如果敢说的话,将返回值改为 bool。这是对AWS 也抱怨的,也许将来会进行更改…所以抱着一线希望将其作为 bool。

_ftell

看起来并没有什么变化。如果敢说的话,文档中提到“这个方法是对应fseek()调用的,用于确定当前位置”,这并不是错误的。通过追踪PHP邮件列表(URL忘记),发现由于一些深远的原因,它是这样编写的,所以这是正确的。关于这一点,我将简要涉及seek的相关内容。

_fseek

看起来并没有什么变化。如果敢说的话,文档中的“当前实现不会将whence的值设置为SEEK_CUR。这样的seek会被内部转换为SEEK_SET”是错误的。确认至少在Windows上,使用文件标志"a"打开fopen时可以通过SEEK_CUR调用确认。也就是说,需要实现SEEK_CUR。

顺便说一句,关于_ftell中提到的tell和seek,以下内容可能有参考价值。

  • 在成功的情况下,在调用streamWrapper::stream_seek()后立即调用streamWrapper::stream_tell()。如果streamWrapper::stream_tell()失败,则调用原函数的返回值设置为false。
_feof

没有什么变化。

_fflush

没有什么变化。

_flock

“锁被阻塞”情况下的接收参数$would_block不对应。因为没有在流端对应该参数,所以无法向调用方传递值。

_fstat

没有什么变化。

_stream_set_blocking

这也像touch一样,由于参数分支,所以将其分到单独的方法中,以便它们被调用。除此之外,并没有什么变化。

_stream_set_read_buffer

这也像touch一样,由于参数分支,所以将其分到单独的方法中,以便它们被调用。除此之外,并没有什么变化。如果敢说的话,文档对ReadBuffer没有任何提及,但它确实被调用了(可能是文档错误?)。另外,返回值是特殊的,“成功时返回0,无法按要求设置时返回其他值”。大多数情况下,可以通过错误消息来判断,所以这里使用了bool类型。

_stream_set_write_buffer

这也像touch一样,由于参数分支,所以将其分到单独的方法中,以便它们被调用。除此之外,并没有什么变化。与_stream_set_read_buffer相同。

_stream_set_timeout

这也像touch一样,由于参数分支,所以将其分到单独的方法中,以便它们被调用。在函数版中,超时指定是$seconds+$microseconds,但为了易于理解,将其合并为float。例如,要将2.5秒设置为超时,应指定为(2.5)而不是(2, 500000)。这可能是原始syscall是裸露的,所以不需要这么高的精度。

此外,实现此方法的需求几乎不存在。虽然可以设置超时并实现超时,但由于没有提供传递给调用方的技巧(stream_get_meta_data的返回值),所以不能实现。如果stream_read有像&$timedout这样的接收参数,则可以实现...

_stream_select

函数版本的 stream_select 函数经常使用,但关于如何映射到流版本的 stream_cast 的信息不足,因此,此函数特别处理,不抛出“未实现异常”,而是使用默认实现 return false。这是因为,如果不使用,则不需要实现。但是,似乎在调用 mime_content_type 时会调用 stream_select(cast)。实际上,返回 false 也能正常工作,所以就这样做了。

对应表

以下是对应表。由于篇幅原因,省略了类型和默认值。

Mixin

实际上,StreamWrapper 并不是简单地实现一个单元就足够了,多个方法相互关联。例如,调用 file_get_contents 会调用 fopenfreadfeof 等各种方法。意想不到的是,调用 include/require 也会调用 stream_set_read_buffer。还有,上述的 mime_content_type 中的 stream_cast 也非常意外。不可能一一实现这些,而且大多数Wrapper的实现几乎相同,因此预先定义了一组trait。

StreamTrait

用于读取和写入流的trait。内部进行缓冲。不太可能存在不进行缓冲的StreamWrapper,大多数Wrapper在执行流操作时都需要缓冲。在这种情况下,只要使用它,就可以完成所有流操作。由于是trait,如果无法处理,则可以单独定义方法。

此外,通常所说的“缓冲区”与这里的略有不同。简单地说,“打开时一次性读取全部,flush时一次性写入全部”的缓冲区。在实际的IO中,部分写入的“真正的”缓冲区实现难度大,而有用性低,在大多数用例中都不需要,因此这样实现。

DirectoryIOTrait

支持目录的scheme的trait。虽然只有mkdir/rmdir两种函数(如果要说的话,rename/stat也),但mkdir有递归处理,rmdir在有内容的情况下不能删除,这些是共有处理,因此用trait提取出来。特别值得注意的是,实现很直接。

DirectoryIteratorTrait

为scandir提供的trait。为了使用scandir,需要实现opendir、readdir、rewinddir、closedir这四种方法,但在现代,IteratorAggregate、Generator、iterable等已经准备好了,因此不需要实现这四种方法。只要提供一个Iterator,所有的实现都应该完成。这是为此提供的trait。如果只是scandir,那么没问题,但如果直接调用rewinddir,则需要传递一个rewindable的Iterator。

另外,DirectoryIOTrait没有关联。即使没有目录的概念,也可以实现“以"/"为标记的目录探索”(S3是很好的例子)。

UrlIOTrait

为基于路径的函数提供输入输出的trait(filesize、rename、unlink等)。没有特别要注意的。实现很直接。

UrlPermissionTrait

为基于路径的函数提供权限(chmod、chown、chgrp)的trait。不执行权限控制。任何人都可以随时更改。理由是下面的实现太麻烦了。

  • chmod: 大多数系统中,只有文件的所有者才能更改其模式
  • chown: 只有超级用户才能更改文件的所有者
  • chgrp: 只有超级用户才能任意更改文件所属的组。其他用户可以将其所属的组更改为其所属的组
隐含的公共API

所有的trait都有一个隐含的API。在代码上用abstract表示。例如,写入处理在UrlIOTrait和StreamTrait中都会使用,存在检查几乎在所有的trait中都会使用。因此,为了避免重复实现,有意将命名统一,以便一些trait中实现的函数可以在其他函数中重用。目前有以下方法。

function parent(): ?string;
function children(...): iterable;
function move(...): void;

URL操作系方法。文件和目录都可能被调用。

parent和children分别返回父URL(string)子URL(iterable)

function getMetadata(...): ?array;
function setMetadata(...): void;

元数据操作系方法。文件和目录都可能被调用。

getMetadata也兼有存在检查。如果条目不存在,则必须返回null。

function createDirectory(...): void;
function deleteDirectory(...): void;

目录操作系方法。如前所述,即使支持目录,也至少需要实现mkdir和rmdir。mkdir有递归选项,rmdir需要空检查。为此需要这些方法。不需要标准函数,或者始终需要递归操作,则不需要这些方法(trait)。在某些情况下,这可能更方便。

function selectFile(...): string;
function createFile(...): void;
function appendFile(...): void;
function deleteFile(...): void;

文件操作系方法。

selectFile有一个包含$metadata的引用参数。需要包含与getMetadata相同的数组。故意使用引用参数是因为在单次操作中可以同时获取元数据和内容,这样就不会浪费(selectFile+getMetadata可以原子操作时尤其如此)。

createFile有一个包含$metadata的参数。需要包含与setMetadata相同的数组。故意使用参数是因为在单次操作中可以同时更新元数据和内容,这样就不会浪费(createFile+setMetadata可以原子操作时尤其如此)。

appendFile是“a”模式下的追加处理。可以一次性读取全部再一次性保存,但有些情况下,可能需要专门的追加处理(例如mysql的CONCAT、redis的APPEND等),这样使用会大大提高效率。更不用说如果追加失败,那么就不应该使用“a”模式。

Stream

Stream中的○○Stream仿佛是这个包的主角,但实际上,所有都是参考实现。作者为了实现多样性而随意实现,并“感觉可以用了”,所以只是配置了一些东西。当然,也可以直接使用,但有很多是硬编码实现,完全没有复杂性。反复强调,本包的主角是顶级目录的interface+trait

不过,有一点点思想,还是介绍一下。

scheme://hostname:port/path/to/?query#file.json
───   ──────  ──── ──  ────
  │          │           │    │      └─  プロトコルにおける「キー名」です
  │          │           │    └─────  プロトコルの「パラメータ」です
  │          │           └────────  プロトコルにおける「内部的な位置」を示します
  │          └─────────────── プロトコルにおける「ネットワーク上の位置」を示します
  └───────────────────── プロトコル名です

比如mysql,那么

  • 网络上的位置:相当于DSN
  • 内部位置:相当于模式·表名·主键
  • 参数:相当于charset等
  • 键名:(如果支持的话)相当于主键

例如 mysql://127.0.0.1:3306/dbname/tablename?charset=utf8mb4#pkval 这样写就很清晰。其他,如果redis,则是 redis://127.0.0.1:6379/dbindex/key,如果是S3,则是 s3://endpoint/bucket/objectname。内置的zip scheme使用fragment表示内部文件,因此是 zip:///path/to/file.zip#localname.txt 的形式。

这样,“通过URL设置KVS规范,使所有协议的StreamWrapper都可以像KVS一样使用”是目标作为 存在 的。但实际上,正如上面所述,是为了实现多样性而提供的参考实现。

首先,如果是使用MySQL,PDO或Doctrine都是有的,所以使用流包装器的必要性几乎为零。S3有AWS官方的流包装器实现。以下是其实施的目的:

  • 全部:仅使用interface+trait无法编写测试,因为“这样做是否真的正确”并不明确,因此有必要实现实际的流(实际上,在实现的过程中产生了多样性)
  • Array:为了高速测试和与file方案的完全兼容
  • Mysql:由于schema full和flock
  • php:出于个人用途
  • Redis:由于schemaless和TTL(context)
  • S3:由于对伪目录的支持
  • Smtp:如果有一个只写流的SMTP将很有趣(几乎是即兴想到的)
  • Zip:由于特殊访问(片段为内部文件名)

许可证

MIT

发布

版本号遵循浪漫版本号规范(不是语义版本号)。

  • 主要版本:在大规模兼容性破坏时升级(架构、类结构的更改等)
  • 次要版本:在小规模兼容性破坏时升级(参数更改、类型提示的添加等)
  • 补丁版本:没有兼容性破坏(默认参数的添加、新类的添加、代码格式等)

1.1.2

  • [feature] 修复php8.2的错误
  • [fixbug] 修复没有扩展名时也会出现点的问题

1.1.1

  • [feature] 可以附加扩展名等附加信息的PHP协议
  • [feature] 将到标准协议的委派trait
  • [feature] 支持URL的本地协议
  • [feature] URL的细分

1.1.0

  • [*change] 简化flock

1.0.0

  • 发布