1. 流的概念
流是对流式数据的抽象,用于统一数据操作,比如文件数据、网络数据、压缩数据等,使其可以共享一套函数。
PHP 的文件系统函数就是这样,比如 file_get_contents()
函数可以打开本地文件,也可以访问 URL 就是这一体现。简单来说,流就是表现出流式数据行为的资源对象。以线性方式进行读写,也可以使用 fseek()
来偏移到流的任意位置。
流有点类似数据库抽象层,在数据库抽象层方面,不管使用什么数据库,在抽象层之上都使用相同的方式操作数据。而流是对数据的抽象,不管是本地文件、远程文件,还是压缩文件等等,只要来的是流式数据,那么操作方式就是一样的。
2. 包装器
2.1 包装器概念
有了流这个概念,就引申出了包装器 wrapper 这个概念,每个流都对应一种包装器。
流是从统一操作这个角度产生的一个概念,而包装器是从理解流数据内容出发产生的一个概念,也就是这个统一的操作方式怎么操作或配置不同的内容;这些内容都是以流的方式呈现,但内容规则是不一样的。
比如 HTTP 协议传来的数据是流的方式,但只有 HTTP 包装器才能理解 HTTP 协议传来的数据的意思,并能操作它。
官方手册说:
包装器是告诉流怎么处理具体协议或编码的附加代码。
包装器可以嵌套,一个流外面包裹了一个包装器后,还可以在外层继续包裹包装器,这个时候里层的包装器相对于外层的包装器而言充当流的角色。
PHP 默认内置很多包装器,同时,你也可以使用 PHP 函数 stream_wrapper_register()
或者直接在扩展里面使用流相关的 API 来自己封装包装器。各种各样的包装器都能添加到 PHP 中,并且使用上没有任何限制。
PHP 支持的协议和包装器请看 这里。
要查看当前已注册的包装器列表,可以使用 stream_get_wrappers()
。
$wrappers = stream_get_wrappers();
print_r($wrappers);
/* output
Array (
0 => 'php',
1 => 'file',
2 => 'glob',
3 => 'data',
4 => 'http',
5 => 'ftp',
6 => 'zip',
7 => 'compress.zlib',
8 => 'compress.bzip2',
9 => 'https',
10 => 'ftps',
11 => 'phar'
)
*/
流的引用:
schema://target
- schema(string) - 要使用的包装器名称。例如:file, http, https, ftp, ftps, compress.zlib, compress.bzip2, 以及 php。如果没有指定,将会使用默认值(通常是
file://
)。 - target - 取决于要使用的包装器。文件系统相关的流,通常是文件路径加上要请求的文件名。网络相关的流,通常是主机名,后面加上一个路径。
2.2 自定义包装器
在用 fopen(),fwrite(),fgets(),rewind(),file_put_contents(),file_get_contents() 等等文件系统函数操作流时,数据是先传给定义的包装器类对象,包装器再去操作流。
那么,如何实现一个自定义的包装器呢?
PHP 提供了一个类原型 streamWrapper
。
官方手册中说:
这不是一个真正的类,只是一个类如何定义内部协议的原型。使用该协议的时候,此类的的实例将会被创建。如果按其它方式实现这些方法,将会导致未定义行为。
类的摘要如下:
streamWrapper {
/* Properties */
public resource $context;
/* Methods */
__construct(void)
__destruct(void)
public bool dir_closedir(void)
public bool dir_opendir(sting $path, int $options)
public string dir_readdir(void)
public bool dir_rewinddir(void)
public bool mkdir(string $path, int $mode, int $options)
public bool rename(string $path_from, string $path_to)
public bool rmdir(string $path, int $options)
public resource stream_cast(int $cast_as)
public void stream_close(void)
public bool stream_eof(void)
public bool stream_flush(void)
public bool stream_lock(int $operation)
public bool stream_metadata(string $path, int $option, mixed $value)
public bool stream_open(string $path, string $mode, int $options, string &$opened_path)
public string stream_read(int $count)
public bool stream_seek(int $offset, int $whence = SEEK_SET)
public bool stream_set_option(int $option, int $arg1, int $arg2)
public array stream_stat(void)
public int stream_tell(void)
public bool stream_truncate(int $new_size)
public int stream_write(string $data)
public bool unlink(string $path)
public array url_stat(string $path, int $flags)
}
这个原型里面定义的方法,根据自己需要去定义,并不要求全部实现,这就是为什么不定义成接口的原因,因为有些实现根本用不着某些方法。
这带来很多灵活性,比如包装器是不支持删除目录 rmdir()
功能的,那么就不需要实现,由于未实现,如果用户在包装器上调用 rmdir()
将有异常抛出,可以自定义这个异常,也可以实现它并在其内部抛出异常。
流系列函数可以参考官方手册 Stream 函数。
2.3 包装器实例
本例实现 var://
包装器,它采用标准的文件系统流函数,如 fread()
,来访问指定的全局变量。
如 URL var://foo
将对全局变量 $GLOBALS['foo']
读取或写入数据。
代码如下:
// VariavleStream.php
namespace Stream;
class VariableStream {
public $position;
public $var_name;
public function stream_open($path, $mode, $options, &$opened_path)
{
$url = parse_url($path);
$this->var_name = $url['host'];
$this->position = 0;
return true;
}
public function stream_read($count)
{
$ret = substr($GLOBALS[$this->var_name], $this->position, $count);
$this->position += strlen($ret);
return $ret;
}
public function stream_write($data)
{
$left = substr($GLOBALS[$this->var_name], 0, $this->position);
$right = substr($GLOBALS[$this->var_name], $this->position + strlen($data));
$GLOBALS[$this->var_name] = $left . $data . $right;
$this->position += strlen($data);
return strlen($data);
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return $this->position >= strlen($GLOBALS[$this->var_name]);
}
public function stream_seek($offset, $whence)
{
switch ($whence) {
case SEEK_SET:
if ($offset < strlen($GLOBALS[$this->var_name]) && $offset >=0) {
$this->position = $offset;
return true;
} else {
return false;
}
break;
case SEEK_END:
if (strlen($GLOBALS[$this->var_name]) + $offset >= 0) {
$this->position = strlen($GLOBALS[$this->var_name]) + $offset;
return true;
} else {
return false;
}
break;
default:
return false;
}
}
public function stream_metadata($path, $option, $var)
{
if ($option == STREAM_META_TOUCH) {
$url = parse_url($path);
$var_name = $url['host'];
if (!isset($GLOBALS[$var_name])) {
$GLOBALS[$var_name] = '';
}
return true;
}
return false;
}
}
// index.php
include 'VariableStream.php';
use Stream\VariableStream;
stream_wrapper_register('var', VariableStream::class)
or die('Failed to register protocol');
$myVar = '';
$fp = fopen('var://myVar', 'r+');
fwrite($fp, "line1\n");
fwrite($fp, "line2\n");
fwrite($fp, "line3\n");
rewind($fp);
while (!feof($fp)) {
echo fgets($fp);
}
fclose($fp);
var_dump($myVar);
/* output
line1
line2
line3
xxx\index.php:31:string 'line1
line2
line3
' (length=18)
*/
3. 过滤器
3.1 过滤器概念
在 PHP 自身底层实现的 C 语言开发文档中有这样的解释:
流 API 操作分为不同级别:在基本级别,API 定义了 php_stream 对象表示流式数据源;在稍微高一点的级别,API 定义了 php_stream_wrapper 对象。它包裹低一级别的 php_stream 对象,以提供取回 URL 的内容和元数据、添加上下文参数的能力,调整包装器行为;每一种流打开以后都可以应用任意数量的过滤器在上面,流数据会经过过滤器的处理。
过滤器这个词用的有些不准确,可能会误导人。
从字面意思看好像是去掉一些数据的感觉,应该称为数据调整器,因为它既可以去掉一些数据,也可以添加,还可以修改,但由于历史原因约定俗成,也就称为过滤器了,大家心里明白就好。
我们经常看到下面的词,来解释下它们的区别:
- 资源和数据:资源是比较宏观的说法,通常包含数据,而数据是比较具象的说法,在开发程序的时候经常说是数据,而在软件规划时说是资源,它们是近义词,就像软件设计和程序开发的区别一样。
- 上下文和参数:上下文是比较宏观的说法,经常用在沟通上面,具体点来讲就是一次沟通本身的参数,而参数这个说法往往用在比较具体的事情上面,比如说函数。
过滤器的基类:php_user_filter。
和包装器类似,自定义过滤器使用 stream_filter_register() 进行注册。
3.2 过滤器实例
/* Define our filter class */
class strtolowerFilter extends php_user_filter {
function filter($in, $out, &$consumed, $closing)
{
while ($bucket = stream_bucket_make_writeable($in)) {
$bucket->data = strtolower($bucket->data);
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
}
/* Register out filter with PHP */
stream_filter_register('strtolower', 'strtolowerFilter')
or die('Failed to register filter');
$fp = fopen('foo-bar.txt', 'w');
/* Attach the registered filter to the stream just opened */
stream_filter_append($fp, 'strtolower');
fwrite($fp, "Line1\n");
fwrite($fp, "Word - 2\n");
fwrite($fp, "Easy As 123\n");
fclose($fp);
/* Read the contents back out */
readfile('foo-bar.txt');
/* output
line1
word - 2
easy as 123
*/