PHP 自定义流包装器
PHP流包装器的部分优势在于能够将我们自己的流包装器添加到可用包装器列表中。因此,我们可以通过注册流包装器然后使用普通fopen()函数来本地打开任何类型的资源。
自定义流包装器功能是通过PHP中内置的几个函数实现的。
stream_get_wrappers()
第一个是函数isstream_get_wrappers(),它返回我们系统上可用的流包装器数组。这是使用中的一个示例。
php -r "print_r(stream_get_wrappers());" Array ( [0] => https [1] => ftps [2] => compress.zlib [3] => compress.bzip2 [4] => php [5] => file [6] => glob [7] => data [8] => http [9] => ftp [10] => phar [11] => zip )
我们可以使用任何这些流类型来创建资源,然后使用它们。
stream_wrapper_unregister()
我们可以使用该stream_wrapper_unregister()函数取消注册任何现有的流。使用这个函数,我们实际上可以通过删除将http地址作为流打开的功能来破坏PHP。以下代码检查http流是否存在,然后将其删除。
$wrapperExists = in_array("http", stream_get_wrappers()); if ($wrapperExists) { stream_wrapper_unregister("http"); }
现在,如果我们尝试通过这样的fopen()函数使用http流。
$stream = fopen('http://www.example.com/', 'r'); echo fread($stream, 50);
我们得到以下错误。
PHPWarning: fopen():Unabletofindthewrapper"http"-didyouforgettoenableitwhenyouconfiguredPHP?
这本身似乎没什么用,但是在添加我们自己的自定义流包装器时它会派上用场。
stream_wrapper_register()
最后,我们有stream_wrapper_register(),我们可以使用它注册自定义流交换器类。不过,首先,我们需要创建一个可以注册为流包装器的类。这个类需要实现一些核心方法。您需要创建的最小类应该实现以下接口。
interface StreamWrapper { /** * This method is called immediately after the wrapper is initialized (f.e. by fopen() and file_get_contents()). * * @param string $path * Specifies the URL that was passed to the original function. * @param string $mode * The mode used to open the file, as detailed for fopen(). * @param int $options * Holds additional flags set by the streams API. It can hold one or more of the following * values OR'd together. * @param string $opened_path * If the path is opened successfully, and STREAM_USE_PATH is set in options, opened_path * should be set to the full path of the file/resource that was actually opened. * * @return bool * Returns TRUE on success or FALSE on failure. */ public function stream_open(string $path, string $mode, int $options, string $opened_path = NULL): bool; /** * This method is called in response to fclose(). * * No value is returned. */ public function stream_close(): void; /** * This method is called in response to fread() and fgets(). * * @param int $count * How many bytes of data from the current position should be returned. * * @return string * If there are less than count bytes available, return as many as are available. If no * more data is available, return either FALSE or an empty string. */ public function stream_read(int $count): string; /** * This method is called in response to feof(). * * @return bool * Should return TRUE if the read/write position is at the end of the stream and if no * more data is available to be read, or FALSE otherwise. */ public function stream_eof(): bool; /** * Seeks to specific location in a stream. * * @param int $offset * The stream offset to seek to. * @param int $whence * Possible values: * - SEEK_SET - Set position equal to offset bytes. * - SEEK_CUR - Set position to current location plus offset. * - SEEK_END - Set position to end-of-file plus offset. * * @return bool * Return TRUE if the position was updated, FALSE otherwise. */ public function stream_seek(int $offset, int $whence = SEEK_SET): bool; /** * Retrieve information about a file resource. * * @return array * See stat(). */ public function stream_stat(): array; }
这个类中还有一些其他方法可用。有关完整列表,请参阅有关流包装器类的PHP文档。
使用该接口,我们可以创建一个简单的类。
在考虑要创建什么样的流包装器类时,我想到了一些想法。由于我们在上一个示例中删除了使用http流的功能,因此我认为重新添加一个模拟http流类是个好主意。
以下类实现了StreamWrapper接口以创建模拟http包装器。这个类所做的就是在读取时返回一个非常简单的html字符串。请注意,该stream_read()方法不仅仅是返回一个字符串。它将第一次返回一个字符串,然后每次返回一个空字符串。PHP文档stream_read() 说明某些函数(如file_get_contents())将不断循环和调用,stream_read()直到它收到一个空字符串,所以我添加了一些逻辑来处理这个问题。
class MockHttpStreamWrapper implements StreamWrapper { protected $streamRead; public function stream_open(string $path, string $mode, int $options, string $opened_path = NULL): bool { //想象一下,我们在这里打开一个流。 return true; } public function stream_close(): void { //想象一下这里的流被关闭了。 } public function stream_read(int $count): string { if ($this->streamRead == true) { //如果我们已经读取了流,则返回一个字符串。 return ''; } //设置我们已读取流的事实。 $this->streamRead = true; //返回一个HTML字符串。 return 'Hello World
'; } public function stream_eof(): bool { //总是返回真。 return true; } function stream_seek(int $offset, int $whence = SEEK_SET): bool { return false; } public function stream_stat(): array { return []; } }
然后可以使用该stream_wrapper_register()函数将此类用作http流包装器。
stream_wrapper_register('http','MockHttpStreamWrapper',STREAM_IS_URL);
我们传入'http'作为流类型和类名MockHttpStreamWrapper作为我们上面定义的类。第三个参数是任何附加标志,如果协议是URL协议(我们在此处设置),则应将其设置为STREAM_IS_URL。这里的默认值为0,即本地流。
请注意,我们首先需要取消注册http流包装器,然后才能注册我们自己的,否则PHP将抛出错误。
$existed = in_array("http", stream_get_wrappers()); if ($existed) { //取消注册http流包装器。 stream_wrapper_unregister("http"); } //注册我们的自定义流包装器。 stream_wrapper_register('http', 'MockHttpStreamWrapper', STREAM_IS_URL); //使用新的自定义流包装器。 $stream = fopen('http://www.example.com/', 'r'); while (false !== ($line = fgets($stream))) { echo $line; } fclose($stream);
此代码输出以下内容。
HelloWorld
%从表面上看,这似乎没什么用。但是,我们在这里所做的就是使用fopen()或拦截通过http的任何通信file_get_contents()。如果您fopen()用于与API交互并希望创建用于测试目的的模拟,这将非常有用。
有一些例子表明这种方法被用于与gluster文件系统、S3资源以及各种不同的数据交互方式进行交互。Drupal8中使用流包装器来提供公共和私有文件系统的接口。这意味着我们可以使用fopen('public://file.txt')在公共文件目录中打开一个文件,而不必让开发人员包含一堆样板代码来将方案转换到一个位置。