Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support accessing built-in filters as callbacks #5

Merged
merged 8 commits into from
Nov 8, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,75 @@ $filter = Filter\prepend($stream, function ($chunk) {
});
```

### fun()

The `fun($filter, $parameters = null)` function can be used to
create a filter function which uses the given built-in `$filter`.

PHP comes with a useful set of [built-in filters](http://php.net/manual/en/filters.php).
Using `fun()` makes accessing these as easy as passing an input string to filter
and getting the filtered output string.

```php
$fun = Filter\fun('string.rot13');

assert('grfg' === $fun('test'));
assert('test' === $fun($fun('test'));
```

Please note that not all filter functions may be available depending on installed
PHP extensions and the PHP version in use.
In particular, [HHVM](http://hhvm.com/) may not offer the same filter functions
or parameters as Zend PHP.
Accessing an unknown filter function will result in a `RuntimeException`:

```php
Filter\fun('unknown'); // throws RuntimeException
```

Some filters may accept or require additional filter parameters.
The optional `$parameters` argument will be passed to the filter handler as-is.
Please refer to the individual filter definition for more details.
For example, the `string.strip_tags` filter can be invoked like this:

```php
$fun = Filter\fun('string.strip_tags', '<a><b>');

$ret = $fun('<b>h<br>i</b>');
assert('<b>hi</b>' === $ret);
```

Under the hood, this function allocates a temporary memory stream, so it's
recommended to clean up the filter function after use.
Also, some filter functions (in particular the
[zlib compression filters](http://php.net/manual/en/filters.compression.php))
may use internal buffers and may emit a final data chunk on close.
The filter function can be closed by invoking without any arguments:

```php
$fun = Filter\fun('zlib.deflate');

$ret = $fun('hello') . $fun('world') . $fun();
assert('helloworld' === gzinflate($ret));
```

The filter function must not be used anymore after it has been closed.
Doing so will result in a `RuntimeException`:

```php
$fun = Filter\fun('string.rot13');
$fun();

$fun('test'); // throws RuntimeException
```

> Note: If you're using the zlib compression filters, then you should be wary
about engine inconsistencies between different PHP versions and HHVM.
These inconsistencies exist in the underlying PHP engines and there's little we
can do about this in this library.
[Our test suite](tests/) contains several test cases that exhibit these issues.
If you feel some test case is missing or outdated, we're happy to accept PRs! :)

### remove()

The `remove($filter)` function can be used to
Expand Down
51 changes: 51 additions & 0 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,57 @@ function prepend($stream, $callback, $read_write = STREAM_FILTER_ALL)
return $ret;
}

/**
* Creates filter fun (function) which uses the given built-in $filter
*
* @param string $filter built-in filter name, see stream_get_filters()
* @param mixed $params additional parameters to pass to the built-in filter
* @return callable a filter callback which can be append()'ed or prepend()'ed
* @throws RuntimeException on error
* @see stream_get_filters()
* @see append()
*/
function fun($filter, $params = null)
{
$fp = fopen('php://memory', 'w');
$filter = @stream_filter_append($fp, $filter, STREAM_FILTER_WRITE, $params);

if ($filter === false) {
fclose($fp);
$error = error_get_last() + array('message' => '');
throw new RuntimeException('Unable to access built-in filter: ' . $error['message']);
}

// append filter function which buffers internally
$buffer = '';
append($fp, function ($chunk) use (&$buffer) {
$buffer .= $chunk;

// always return empty string in order to skip actually writing to stream resource
return '';
}, STREAM_FILTER_WRITE);

$closed = false;

return function ($chunk = null) use ($fp, $filter, &$buffer, &$closed) {
if ($closed) {
throw new \RuntimeException('Unable to perform operation on closed stream');
}
if ($chunk === null) {
$closed = true;
$buffer = '';
fclose($fp);
return $buffer;
}
// initialize buffer and invoke filters by attempting to write to stream
$buffer = '';
fwrite($fp, $chunk);

// buffer now contains everything the filter function returned
return $buffer;
};
}

/**
* remove a callback filter from the given stream
*
Expand Down
28 changes: 23 additions & 5 deletions tests/FilterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,24 @@ public function testRemoveFilter()
fclose($stream);
}

public function testAppendFunDechunk()
{
if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM (dechunk filter does not exist)');

$stream = $this->createStream();

StreamFilter\append($stream, StreamFilter\fun('dechunk'), STREAM_FILTER_WRITE);

fwrite($stream, "2\r\nhe\r\n");
fwrite($stream, "3\r\nllo\r\n");
fwrite($stream, "0\r\n\r\n");
rewind($stream);

$this->assertEquals('hello', stream_get_contents($stream));

fclose($stream);
}

public function testAppendThrows()
{
$this->createErrorHandler($errors);
Expand Down Expand Up @@ -257,8 +275,8 @@ public function testAppendThrowsDuringEnd()

// We can only assert we're not seeing an exception here…
// * php 5.3-5.6 sees one error here
// * php 7 does not any error here
// * hhvm seems the same error twice
// * php 7 does not see any error here
// * hhvm sees the same error twice
//
// If you're curious:
//
Expand Down Expand Up @@ -316,7 +334,7 @@ public function testAppendThrowsShouldTriggerEndButIgnoreExceptionDuringEnd()
*/
public function testAppendInvalidStreamIsRuntimeError()
{
if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM');
if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM (does not reject invalid stream)');
StreamFilter\append(false, function () { });
}

Expand All @@ -325,7 +343,7 @@ public function testAppendInvalidStreamIsRuntimeError()
*/
public function testPrependInvalidStreamIsRuntimeError()
{
if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM');
if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM (does not reject invalid stream)');
StreamFilter\prepend(false, function () { });
}

Expand All @@ -334,7 +352,7 @@ public function testPrependInvalidStreamIsRuntimeError()
*/
public function testRemoveInvalidFilterIsRuntimeError()
{
if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM');
if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM (does not reject invalid filters)');
StreamFilter\remove(false);
}

Expand Down
34 changes: 34 additions & 0 deletions tests/FunTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

use Clue\StreamFilter as Filter;

class FunTest extends PHPUnit_Framework_TestCase
{
public function testFunInRot13()
{
$rot = Filter\fun('string.rot13');

$this->assertEquals('grfg', $rot('test'));
$this->assertEquals('test', $rot($rot('test')));
$this->assertEquals(null, $rot());
}

/**
* @expectedException RuntimeException
*/
public function testFunWriteAfterCloseRot13()
{
$rot = Filter\fun('string.rot13');

$this->assertEquals(null, $rot());
$rot('test');
}

/**
* @expectedException RuntimeException
*/
public function testFunInvalid()
{
Filter\fun('unknown');
}
}
79 changes: 79 additions & 0 deletions tests/FunZlibTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

use Clue\StreamFilter;

class BuiltInZlibTest extends PHPUnit_Framework_TestCase
{
public function testFunZlibDeflateHelloWorld()
{
$deflate = StreamFilter\fun('zlib.deflate');

$data = $deflate('hello') . $deflate(' ') . $deflate('world') . $deflate();

$this->assertEquals(gzdeflate('hello world'), $data);
}

public function testFunZlibDeflateEmpty()
{
if (PHP_VERSION >= 7) $this->markTestSkipped('Not supported on PHP7 (empty string does not invoke filter)');

$deflate = StreamFilter\fun('zlib.deflate');

//$data = gzdeflate('');
$data = $deflate();

$this->assertEquals("\x03\x00", $data);
}

public function testFunZlibDeflateBig()
{
$deflate = StreamFilter\fun('zlib.deflate');

$n = 1000;
$expected = str_repeat('hello', $n);

$bytes = '';
for ($i = 0; $i < $n; ++$i) {
$bytes .= $deflate('hello');
}
$bytes .= $deflate();

$this->assertEquals($expected, gzinflate($bytes));
}

public function testFunZlibInflateHelloWorld()
{
$inflate = StreamFilter\fun('zlib.inflate');

$data = $inflate(gzdeflate('hello world')) . $inflate();

$this->assertEquals('hello world', $data);
}

public function testFunZlibInflateEmpty()
{
$inflate = StreamFilter\fun('zlib.inflate');

$data = $inflate("\x03\x00") . $inflate();

$this->assertEquals('', $data);
}

public function testFunZlibInflateBig()
{
if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM (final chunk will not be emitted)');

$inflate = StreamFilter\fun('zlib.inflate');

$expected = str_repeat('hello', 10);
$bytes = gzdeflate($expected);

$ret = '';
foreach (str_split($bytes, 2) as $chunk) {
$ret .= $inflate($chunk);
}
$ret .= $inflate();

$this->assertEquals($expected, $ret);
}
}