02 December 2016

介绍

在 php 中,很少有见过 stream 的用法,直到我在看 walu 的 php 扩展书 中看到 stream 那一节,才知道 php 有好多不了解的地方。因此在网上找到了此文,严格来说这不是一篇翻译。

原文:using-php-streams-effectively

在这篇文章之前,作者有写过另外一篇入门的 understanding-streams-in-php ,该文章有一个翻译 理解 PHP 中的 Streams

Using Filters

在上文 (即理解 php 中的 streams 那篇文章) ,我们知道 filters 过滤器 可以对 读入数据 或者 把数据写到文件 时进行修改。

php 自带有一些非常好用的过滤器 例如 string.toupper, string.tolower or string.strip_tags 。一些 php 扩展也提供它们自己的的过滤器 。例如, mcrypt 这个扩展 提供了 mcrypt.* 和 mdecrypt.* 过滤器。我们可以用函数 stream_get_filters() 这个函数来列出当前可用的过滤器。

当我们要使用某个过滤器的时候,我们可以用函数 stream_filter_append() 来给数据追加使用更多的过滤器:

$h = fopen('lorem.txt', 'r');

stream_filter_append($h, 'convert.base64-encode');
fpassthru($h);         //  Output all remaining data on a file pointer

fclose($h);

也可以用 php://filter 这个 wrapper 来实现:

$filter = 'convert.base64-encode';
$file   = 'lorem.txt';

$h = fopen("php://filter/read={$filter}/resource=${file}", 'r');

fpassthru($h);         //  Output all remaining data on a file pointer

fclose($h);

Filtering data on read-time: the Markdown filter

第一个自定义的 过滤器 将会作用于 reading stream ,它将 markdown 格式的数据 转换成 HTML 格式。

php 提供了 php_user_filter 这个基类来供我们使用。这个类有两个属性: filtername 和 params
filtername 包含了在使用 stream_filter_register() 来注册过滤器时候所使用的名字,
params 可以通过 stream_filter_append() 来传递参数给过滤器

最主要的方法是 filter() , 这个方法接收 4 个参数:

$in           一组 bucket 对象,包含了要过滤的数据
$out          一组 bucket 对象,存储了过滤之后的数据
$consumed     一个计数,它通过引用传递,每次循环的时候都要加上当前处理的数据长度
$closing      一个 bool 变量,stream 将要关闭的时候,这个值为 true

另外两个方法是 onCreate()onClose() , 这两个方法分别在 类实例 被创建和销毁 的时候调用。使用场景是当我们的类实例需要创建资源,并且需要在最后的时候释放资源。

下面要实现的过滤器使用上面的 3 个方法来处理数据,这些数据在属性 $bufferHandle 中保存。如果这些数据是非法的,onCreate() 方法会返回 false, 并且 onClose() 方法会释放相应的资源。

MarkdownFilter 使用了 Michel Fortin 的 markdown 解析器

➜ vi composer.json
{
    "autoload": {
        "psr-4": {
            "OurFilter\\": "src/"
        }
    }
}

➜ composer install
➜ composer require michelf/php-markdown

➜ ls .
index.php
src/MarkDownFilter.php
...

src/MarkDownFilter.php

<?php

namespace OurFilter;

use Michelf\MarkdownExtra;
use php_user_filter;

class MarkDownFilter extends php_user_filter
{
    private $bufferHandle;

    public function filter($in, $out, &$consumed, $closing) {
        $data = '';

        while ($inBucket = stream_bucket_make_writeable($in)) {
            $data .= $inBucket->data;
            $consumed += $inBucket->datalen;
        }

        $outBucket = stream_bucket_new($this->bufferHandle, '');

        if (false === $outBucket) {
            return PSFS_ERR_FATAL;
        }

        $parser          = new MarkdownExtra;
        $html            = $parser->transform($data);
        $outBucket->data = $html;

        stream_bucket_append($out, $outBucket);

        return PSFS_PASS_ON;
    }

    public function onCreate() {
        $this->bufferHandle = fopen('php://temp', 'w+');
        if (false === $this->bufferHandle) {
            return false;
        }

        return true;
    }

    public function onClose() {
        fclose($this->bufferHandle);
    }
}

index.php

require_once __DIR__ .'/vendor/autoload.php';

use MarkdownFilter\MarkDownFilter;

$markdownFilter = new MarkDownFilter();

stream_filter_register("markdown", MarkDownFilter::class);

$content = file_get_contents('php://filter/read=markdown/resource=file:///path/to/somefile.md');

if (false === $content) {
    echo "Unable to read from source\n";
    exit(1);
}

echo $content;
➜ php index.php

Filtering data on write-time: the Template filter

现在我们已经可以让 Markdown 转换成 HTML 了, 我们需要将生成后的 HTML 写入到一个文件中。

在上面,我们用 input 过滤器来实现了 read-and-convert,现在我们要用类似的方法,在 output stream 中嵌入模板引擎来实现 convert-and-save。


在这个教程中,使用了 RainTPL 这个解析器


template 的结构和我们的 输入过滤器是相似的。首先,我们要注册我们的过滤器:

stream_filter_register("template.*", TemplateFilter::class);

我们使用 filtername.* 作为 过滤器标签,这样我们可以使用 * 来传递一些数据给 类实例。
这是必须的,因为似乎没有其他方式通过 php://filter wrapper 来传递参数给过滤器。

这个过滤可以这样被调用:

$encodeText = base64_encode('Some Document Title');
$result     = file_put_contents("php://filter/write=template.{$encodeText}/resource=file:///path/to/somefile.html", $content);

// 在下面,我们可以用 list(, $title) = explode('.', $this->filtername); 来获取这个 encodeText

我们将一个 markdown 的 title 作为参数传递给过滤器名字的第二个部分,并且这会通过 onCreate() 方法来处理。

➜ composer require rain/raintpl

src/TemplateFilter.php

<?php

namespace OurFilter;

use Rain\Tpl;
use php_user_filter;

class TemplateFilter extends php_user_filter
{
    private $bufferHandle;
    private $docTitle;

    public function filter($in, $out, &$consumed, $closing) {
        $data = '';

        while ($inBucket = stream_bucket_make_writeable($in)) {
            $data .= $inBucket->data;
            $consumed += $inBucket->datalen;
        }

        $outBucket = stream_bucket_new($this->bufferHandle, '');

        if (false === $outBucket) {
            return PSFS_ERR_FATAL;
        }

        $config = array(
            "tpl_dir"     => dirname(__FILE__) . "/templates/",    // 需要在当前的目录下新建一个 templates 文件夹
            "cache_dir"   => dirname(__FILE__) . "/cache/",        // 需要在当前的目录下新建一个 cache 文件夹
            "auto_escape" => false
        );

        Tpl::configure($config);

        $view = new Tpl();
        $view->assign('title', $this->docTitle);
        $view->assign('body', $data);

        $content         = $view->draw('default', true);
        $outBucket->data = $content;

        stream_bucket_append($out, $outBucket);

        return PSFS_PASS_ON;
    }

    public function onCreate() {
        $this->bufferHandle = fopen('php://temp', 'w+');
        if (false === $this->bufferHandle) {
            return false;
        }

        list(, $title) = explode('.', $this->filtername);   // template.xxx

        $this->docTitle = base64_decode($title);            // xxx

        return true;
    }

    public function onClose() {
        fclose($this->bufferHandle);
    }
}

在 onCreate() 方法中, 当 buffer stream 被初始化时,通过将 $this->filtername 进行一个 explode 操作,然后获取到一开始就进行 base64_encode 加密后的数据。接下来,模板解析器加载了相应的配置信息,配置 template 和 cache 目录,还有取消自动 html 转义

➜ cat src/templates/default.html
<h1>
  {$title}
</h1>

<body>
  {$body}
</body>

➜ php index.php
➜ cat /path/to/somefile.html