Note: Legacy PHP versions (PHP < 5.4) do not support passing additional data * from the end signal handler if the stream is being closed. * * If your callback throws an `Exception`, then the filter process will be aborted. * In order to play nice with PHP's stream handling, * the `Exception` will be transformed to a PHP warning instead: * * ```php * Filter\append($stream, function ($chunk) { * throw new \RuntimeException('Unexpected chunk'); * }); * * // raises an E_USER_WARNING with "Error invoking filter: Unexpected chunk" * fwrite($stream, 'hello'); * ``` * * The optional `$read_write` parameter can be used to only invoke the `$callback` * when either writing to the stream or only when reading from the stream: * * ```php * Filter\append($stream, function ($chunk) { * // will be called each time you write to the stream * return $chunk; * }, STREAM_FILTER_WRITE); * * Filter\append($stream, function ($chunk) { * // will be called each time you read from the stream * return $chunk; * }, STREAM_FILTER_READ); * ``` * * This function returns a filter resource which can be passed to [`remove()`](#remove). * * > Note that once a filter has been added to stream, the stream can no longer be passed to * > [`stream_select()`](https://www.php.net/manual/en/function.stream-select.php) * > (and family). * > * > > Warning: stream_select(): cannot cast a filtered stream on this system in {file} on line {line} * > * > This is due to limitations of PHP's stream filter support, as it can no longer reliably * > tell when the underlying stream resource is actually ready. * > As an alternative, consider calling `stream_select()` on the unfiltered stream and * > then pass the unfiltered data through the [`fun()`](#fun) function. * * @param resource $stream * @param callable $callback * @param int $read_write * @return resource filter resource which can be used for `remove()` * @throws \Exception on error * @uses stream_filter_append() */ function append($stream, $callback, $read_write = STREAM_FILTER_ALL) { $ret = @\stream_filter_append($stream, register(), $read_write, $callback); // PHP 8 throws above on type errors, older PHP and memory issues can throw here // @codeCoverageIgnoreStart if ($ret === false) { $error = \error_get_last() + array('message' => ''); throw new \RuntimeException('Unable to append filter: ' . $error['message']); } // @codeCoverageIgnoreEnd return $ret; } /** * Prepend a filter callback to the given stream. * * Each stream can have a list of filters attached. * This function prepends a filter to the start of this list. * * If the given filter can not be added, it throws an `Exception`. * * ```php * $filter = Filter\prepend($stream, function ($chunk) { * // will be called each time you read or write a $chunk to/from the stream * return $chunk; * }); * ``` * * This function returns a filter resource which can be passed to [`remove()`](#remove). * * Except for the position in the list of filters, this function behaves exactly * like the [`append()`](#append) function. * For more details about its behavior, see also the [`append()`](#append) function. * * @param resource $stream * @param callable $callback * @param int $read_write * @return resource filter resource which can be used for `remove()` * @throws \Exception on error * @uses stream_filter_prepend() */ function prepend($stream, $callback, $read_write = STREAM_FILTER_ALL) { $ret = @\stream_filter_prepend($stream, register(), $read_write, $callback); // PHP 8 throws above on type errors, older PHP and memory issues can throw here // @codeCoverageIgnoreStart if ($ret === false) { $error = \error_get_last() + array('message' => ''); throw new \RuntimeException('Unable to prepend filter: ' . $error['message']); } // @codeCoverageIgnoreEnd return $ret; } /** * Create a filter function which uses the given built-in `$filter`. * * PHP comes with a useful set of [built-in filters](https://www.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](https://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 – most * filters do not require filter parameters. * If given, the optional `$parameters` argument will be passed to the * underlying filter handler as-is. * In particular, note how *not passing* this parameter at all differs from * explicitly passing a `null` value (which many filters do not accept). * 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', ''); * * $ret = $fun('h
i
'); * assert('hi' === $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](https://www.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! :) * * @param string $filter built-in filter name. See stream_get_filters() or http://php.net/manual/en/filters.php * @param mixed $parameters (optional) parameters to pass to the built-in filter as-is * @return callable a filter callback which can be append()'ed or prepend()'ed * @throws \RuntimeException on error * @link http://php.net/manual/en/filters.php * @see stream_get_filters() * @see append() */ function fun($filter, $parameters = null) { $fp = \fopen('php://memory', 'w'); if (\func_num_args() === 1) { $filter = @\stream_filter_append($fp, $filter, \STREAM_FILTER_WRITE); } else { $filter = @\stream_filter_append($fp, $filter, \STREAM_FILTER_WRITE, $parameters); } 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 filter previously added via `append()` or `prepend()`. * * ```php * $filter = Filter\append($stream, function () { * // … * }); * Filter\remove($filter); * ``` * * @param resource $filter * @return bool true on success or false on error * @throws \RuntimeException on error * @uses stream_filter_remove() */ function remove($filter) { if (@\stream_filter_remove($filter) === false) { // PHP 8 throws above on type errors, older PHP and memory issues can throw here $error = \error_get_last(); throw new \RuntimeException('Unable to remove filter: ' . $error['message']); } } /** * Registers the callback filter and returns the resulting filter name * * There should be little reason to call this function manually. * * @return string filter name * @uses CallbackFilter */ function register() { static $registered = null; if ($registered === null) { $registered = 'stream-callback'; \stream_filter_register($registered, __NAMESPACE__ . '\CallbackFilter'); } return $registered; }