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

*/