A simple and modern approach to stream filtering in PHP

Overview

clue/stream-filter

CI status installs on Packagist

A simple and modern approach to stream filtering in PHP

Table of contents

Why?

PHP's stream filtering system is great!

It offers very powerful stream filtering options and comes with a useful set of built-in filters. These filters can be used to easily and efficiently perform various transformations on-the-fly, such as:

  • read from a gzip'ed input file,
  • transcode from ISO-8859-1 (Latin1) to UTF-8,
  • write to a bzip output file
  • and much more.

But let's face it: Its API is difficult to work with and its documentation is subpar. This combined means its powerful features are often neglected.

This project aims to make these features more accessible to a broader audience.

  • Lightweight, SOLID design - Provides a thin abstraction that is just good enough and does not get in your way. Custom filters require trivial effort.
  • Good test coverage - Comes with an automated tests suite and is regularly tested in the real world

Support us

We invest a lot of time developing, maintaining and updating our awesome open-source projects. You can help us sustain this high-quality of our work by becoming a sponsor on GitHub. Sponsors get numerous benefits in return, see our sponsoring page for details.

Let's take these projects to the next level together! 🚀

Usage

This lightweight library consists only of a few simple functions. All functions reside under the Clue\StreamFilter namespace.

The below examples refer to all functions with their fully-qualified names like this:

Clue\StreamFilter\append(…);

As of PHP 5.6+ you can also import each required function into your code like this:

use function Clue\StreamFilter\append;

append(…);

Alternatively, you can also use an import statement similar to this:

use Clue\StreamFilter as Filter;

Filter\append(…);

append()

The append(resource $stream, callable $callback, int $read_write = STREAM_FILTER_ALL): resource function can be used to append a filter callback to the given stream.

Each stream can have a list of filters attached. This function appends a filter to the end of this list.

If the given filter can not be added, it throws an Exception.

The $stream can be any valid stream resource, such as:

$stream = fopen('demo.txt', 'w+');

The $callback should be a valid callable function which accepts an individual chunk of data and should return the updated chunk:

$filter = Clue\StreamFilter\append($stream, function ($chunk) {
    // will be called each time you read or write a $chunk to/from the stream
    return $chunk;
});

As such, you can also use native PHP functions or any other callable:

Clue\StreamFilter\append($stream, 'strtoupper');

// will write "HELLO" to the underlying stream
fwrite($stream, 'hello');

If the $callback accepts invocation without parameters, then this signature will be invoked once ending (flushing) the filter:

Clue\StreamFilter\append($stream, function ($chunk = null) {
    if ($chunk === null) {
        // will be called once ending the filter
        return 'end';
    }
    // will be called each time you read or write a $chunk to/from the stream
    return $chunk;
});

fclose($stream);

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:

Clue\StreamFilter\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:

Clue\StreamFilter\append($stream, function ($chunk) {
    // will be called each time you write to the stream
    return $chunk;
}, STREAM_FILTER_WRITE);

Clue\StreamFilter\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().

Note that once a filter has been added to stream, the stream can no longer be passed to stream_select() (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() function.

prepend()

The prepend(resource $stream, callable $callback, int $read_write = STREAM_FILTER_ALL): resource function can be used to 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.

$filter = Clue\StreamFilter\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().

Except for the position in the list of filters, this function behaves exactly like the append() function. For more details about its behavior, see also the append() function.

fun()

The fun(string $filter, mixed $parameters = null): callable 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. Using fun() makes accessing these as easy as passing an input string to filter and getting the filtered output string.

$fun = Clue\StreamFilter\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 may not offer the same filter functions or parameters as Zend PHP. Accessing an unknown filter function will result in a RuntimeException:

Clue\StreamFilter\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:

$fun = Clue\StreamFilter\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) may use internal buffers and may emit a final data chunk on close. The filter function can be closed by invoking without any arguments:

$fun = Clue\StreamFilter\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:

$fun = Clue\StreamFilter\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 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(resource $filter): bool function can be used to remove a filter previously added via append() or prepend().

$filter = Clue\StreamFilter\append($stream, function () {
    // …
});
Clue\StreamFilter\remove($filter);

Install

The recommended way to install this library is through Composer. New to Composer?

This project follows SemVer. This will install the latest supported version:

$ composer require clue/stream-filter:^1.6

See also the CHANGELOG for details about version upgrades.

This project aims to run on any platform and thus does not require any PHP extensions and supports running on legacy PHP 5.3 through current PHP 8+ and HHVM. It's highly recommended to use the latest supported PHP version for this project. Older PHP versions may suffer from a number of inconsistencies documented above.

Tests

To run the test suite, you first need to clone this repo and then install all dependencies through Composer:

$ composer install

To run the test suite, go to the project root and run:

$ vendor/bin/phpunit

License

This project is released under the permissive MIT license.

Did you know that I offer custom development services and issuing invoices for sponsorships of releases and for contributions? Contact me (@clue) for details.

Comments
  • Do not pass

    Do not pass "null" to stream_filter_append

    Some filters do not support null being passed. Their default values are an empty array or nothing.

    If you run the test without the patch you would get:

    1) FunTest::testFunInQuotedPrintable
    RuntimeException: Unable to access built-in filter: stream_filter_append(): unable to create or locate filter "convert.quoted-printable-encode"
    
    /path/php-stream-filter/src/functions.php:73
    /path/php-stream-filter/tests/FunTest.php:18
    

    See the original issue here: https://github.com/php-http/message/issues/78

    bug new feature 
    opened by Nyholm 17
  • Check if the function is declared before declaring it.

    Check if the function is declared before declaring it.

    In case you use pthreads or something else related to threading you might encounter this issue. It happens only with functions and is fixable only by adding these checks. Another way to avoid this is to wrap functions as static into classes.

    bug help wanted easy pick 
    opened by NikoGrano 14
  • [Question] Why should we call stream_bucket_make_writeable inside a loop?

    [Question] Why should we call stream_bucket_make_writeable inside a loop?

    Every single implementation of php_user_filter I see calls the stream_bucket_make_writeable function inside a loop but in every single test I made it doesn't make any difference. The second call to stream_bucket_make_writeable always returns null in my tests.

    In which case is this loop really needed?

    question 
    opened by CViniciusSDias 6
  • Adding test with base64 encode

    Adding test with base64 encode

    Im not sure why this test fails... I want some help to solve this...

    1) FunTest::testFunInBase64
    Failed asserting that two strings are equal.
    --- Expected
    +++ Actual
    @@ @@
    -'dGVzdA=='
    +'dGVz'
    
    /path/php-stream-filter/tests/FunTest.php:41
    
    
    opened by Nyholm 6
  • Fatal error when clue/php-stream-filter is loaded in 2 different projects

    Fatal error when clue/php-stream-filter is loaded in 2 different projects

    We have several projects that runs together, and more than one project is using clue/php-stream-filter. This causes a fatal error:

    Fatal error: Cannot redeclare Clue\StreamFilter\append() (previously declared in...

    The reason is that we will load the same functions in the same namespace at 2 different places.

    Before to define a function, we need to check if it does not already exists, like in this project https://github.com/tightenco/collect/blob/laravel-5.8-ongoing/src/Collect/Support/helpers.php#L7.

    opened by strategio 5
  • PHP 8.1 Deprecation: Clue\StreamFilter\CallbackFilter::onClose() should either be compatible with php_user_filter::onClose()

    PHP 8.1 Deprecation: Clue\StreamFilter\CallbackFilter::onClose() should either be compatible with php_user_filter::onClose()

    When I run this library with php 8.1.0 I get

    Return type of Clue\StreamFilter\CallbackFilter::onClose() should either be compatible with php_user_filter::onClose(): void, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice 
    in /vendor/clue/stream-filter/src/CallbackFilter.php:32
    

    I think if compatibility with php 5 is still desirable the attribute annotation should be added.

    new feature 
    opened by rodrigoaguilera 3
  • Able to determine chunk progress (last bucket of stream) ?

    Able to determine chunk progress (last bucket of stream) ?

    Is there any way to determine whether the current chunk is the last in the stream?

    In my case, I need to perform some transformation to only the very last chunk of data. In my current own-rolled filter, I'd be able to keep track of the current consumed amount to determine whether the current bucket were the last in the stream and treat it accordingly. It would be fantastic to be able to use this library, as it solves some other issues with my current filters.

    Many thanks

    question 
    opened by locksem 1
  • Ignore files when exporting package

    Ignore files when exporting package

    This commit is part of a campaign to reduce the amount of data transferred to save global bandwidth and reduce the amount of CO2. See https://github.com/Codeception/Codeception/pull/5527 for more info.

    maintenance 
    opened by simonschaufi 1
  • Ability to filter very large files

    Ability to filter very large files

    First I wanted to compliment the ReactPHP project and all the work you guys are doing - it's really awesome - I'm late to the game but am getting started with a project to compress and decompress large log (and other) files.

    This is a question and not an issue.

    I'm looking to use the zlib.deflate stream filter in an async manner since I'm dealing with rather large files and my search lead me here - looking at the source it looks like the Clue\StreamFilter\fun(...) function buffers the data read from the stream - I just wanted to double check that this could cause an out of memory exception on REALLY large files, yes?

    If so - I've got a few workarounds in mind but wanted to check with the source before I started down a different path.

    Thank You!!

    question 
    opened by nogoodcode 1
  • Base64 test

    Base64 test

    This test would have failed in v1.3. I've made the change suggested in https://github.com/clue/php-stream-filter/pull/17#issuecomment-312462531

    I also made an extra heading to make it more clear that you have to clean the buffer (as suggested here https://github.com/clue/php-stream-filter/pull/17#issuecomment-312485846).

    This will replace #17

    maintenance easy pick 
    opened by Nyholm 1
  • read from a gzip'ed input file

    read from a gzip'ed input file

    the readme mentioned "read from a gzip'ed input file", do you have any example how the code look like for it?

    I would like to read a GZIPPED resource via url in my stream.

    with the regular php filter functions one often gets a "stream_filter_append(): Filter failed to process pre-buffered data" error, because the inflate filter cannot work the the additional gzip header.

    If this lib is aimed for ease of use, I would expect that it bundles a mechanism which allows to read gzipped content.

    Atm I am using

    $url = 'http://localhost/dev/fileget_zlib/packages.json';
    $fp = fopen($url, 'r', null, $ctx);
    Filter\append($fp, 'zlib.inflate');
    echo stream_get_contents($fp);
    

    which triggers a exception: Fatal error: Uncaught exception 'InvalidArgumentException' with message 'No valid callback parameter given to stream_filter_(append|prepend)'

    thanks for the great lib.

    question 
    opened by staabm 1
Releases(v1.6.0)
  • v1.6.0(Feb 21, 2022)

    • Feature: Support PHP 8.1 release. (#45 by @clue)

    • Improve documentation to use fully-qualified function names. (#43 by @SimonFrings and #42 by @PaulRotmann)

    • Improve test suite and use GitHub actions for continuous integration (CI). (#39 and #40 by @SimonFrings)

    Source code(tar.gz)
    Source code(zip)
  • v1.5.0(Oct 2, 2020)

    • Feature: Improve performance by using global imports. (#38 by @clue)

    • Improve API documentation and add support / sponsorship info. (#30 by @clue and #35 by @SimonFrings)

    • Improve test suite and add .gitattributes to exclude dev files from exports. Prepare PHP 8 support, update to PHPUnit 9 and simplify test matrix. (#32 and #37 by @clue and #34 and #36 by @SimonFrings)

    Source code(tar.gz)
    Source code(zip)
  • v1.4.1(Apr 9, 2019)

    • Fix: Check if the function is declared before declaring it. (#23 by @Niko9911)

    • Improve test suite to also test against PHP 7.2 and add test for base64 encoding and decoding filters. (#22 by @arubacao and #25 by @Nyholm and @clue)

    Source code(tar.gz)
    Source code(zip)
  • v1.4.0(Aug 18, 2017)

    • Feature / Fix: The fun() function does not pass filter parameter null to underlying stream_filter_append() by default (#15 by @Nyholm)

      Certain filters (such as convert.quoted-printable-encode) do not accept a filter parameter at all. If no explicit filter parameter is given, we no longer pass a default null value.

      $encode = Filter\fun('convert.quoted-printable-encode');
      assert('t=C3=A4st' === $encode('täst'));
      
    • Add examples and improve documentation (#13 and #20 by @clue and #18 by @Nyholm)

    • Improve test suite by adding PHPUnit to require-dev, fix HHVM build for now again and ignore future HHVM build errors, lock Travis distro so new future defaults will not break the build and test on PHP 7.1 (#12, #14 and #19 by @clue and #16 by @Nyholm)

    Source code(tar.gz)
    Source code(zip)
  • v1.3.0(Nov 8, 2015)

    • Feature: Support accessing built-in filters as callbacks (#5 by @clue)

      $fun = Filter\fun('zlib.deflate');
      
      $ret = $fun('hello') . $fun('world') . $fun();
      assert('helloworld' === gzinflate($ret));
      
    Source code(tar.gz)
    Source code(zip)
  • v1.2.0(Oct 23, 2015)

  • v1.1.0(Oct 23, 2015)

    • Feature: Abort filter operation when catching an Exception (#10 by @clue)
    • Feature: Additional safeguards to prevent filter state corruption (#7 by @clue)
    Source code(tar.gz)
    Source code(zip)
  • v1.0.0(Oct 18, 2015)

Owner
Christian Lück
@reactphp maintainer / Freelance Software Engineer, all things web, passionate about coaching others building awesome things… Reach out if you need help
Christian Lück
An opinioned approach to extend the laravel seed classes.

Laravel Seed Extender A highly opinioned way to work with the laravel seeder. Installation Require the package using composer: composer require touhid

Touhidur Rahman 9 Jan 20, 2022
This project uses dflydev/dot-access-data to provide simple output filtering for cli applications.

FilterViaDotAccessData This project uses dflydev/dot-access-data to provide simple output filtering for applications built with annotated-command / Ro

Consolidation 44 Jul 19, 2022
A simple package to forward Laravel application logs to a Kinesis stream

Laravel Monolog Kinesis Driver A simple package to forward Laravel application logs to a Kinesis stream. Installation Require the package with compose

Pod Point 34 Sep 6, 2022
A base API controller for Laravel that gives sorting, filtering, eager loading and pagination for your resources

Bruno Introduction A Laravel base controller class and a trait that will enable to add filtering, sorting, eager loading and pagination to your resour

Esben Petersen 165 Sep 16, 2022
Eloquent Befriended brings social media-like features like following, blocking and filtering content based on following or blocked models.

Laravel Befriended Eloquent Befriended brings social media-like features like following, blocking and filtering content based on following or blocked

Renoki Co. 720 Jan 3, 2023
Advanced Laravel models filtering capabilities

Advanced Laravel models filtering capabilities Installation You can install the package via composer: composer require pricecurrent/laravel-eloquent-f

Andrew Malinnikov 162 Oct 30, 2022
Library that offers Input Filtering based on Annotations for use with Objects. Check out 2.dev for 2.0 pre-release.

DMS Filter Component This library provides a service that can be used to filter object values based on annotations Install Use composer to add DMS\Fil

Rafael Dohms 89 Nov 28, 2022
In Laravel, we commonly face the problem of adding repetitive filtering code, this package will address this problem.

Filterable In Laravel, we commonly face the problem of adding repetitive filtering code, sorting and search as well this package will address this pro

Zoran Shefot Bogoevski 1 Jun 21, 2022
A non-blocking stream abstraction for PHP based on Amp.

amphp/byte-stream is a stream abstraction to make working with non-blocking I/O simple. Installation This package can be installed as a Composer depen

Amp 317 Dec 22, 2022
An event stream library based on tail

TailEventStream An event stream library based on tail. Note: I don't think you should use this library in a real project, but it's great for education

Matthias Noback 4 Feb 19, 2022
A modern solution for running Laravel Horizon with a CRON-based supervisor.

A modern solution for running Laravel Horizon with a cron-based supervisor This Laravel package automatically checks every three minutes if your Larav

Ralph J. Smit 31 Dec 9, 2022
Use ESM with importmap to manage modern JavaScript in Laravel without transpiling or bundling

Introduction Use ESM with importmap to manage modern JavaScript in Laravel without transpiling or bundling. Inspiration This package was inspired by t

Tony Messias 91 Dec 30, 2022
Validate your input data in a simple way, an easy way and right way. no framework required. For simple or large. project.

wepesi_validation this module will help to do your own input validation from http request POST or GET. INTEGRATION The integration is the simple thing

Boss 4 Dec 17, 2022
Fast and simple implementation of a REST API based on the Laravel Framework, Repository Pattern, Eloquent Resources, Translatability, and Swagger.

Laravel Headless What about? This allows a fast and simple implementation of a REST API based on the Laravel Framework, Repository Pattern, Eloquent R

Julien SCHMITT 6 Dec 30, 2022
Simple address and contact management for Laravel with automatically geocoding to add longitude and latitude

Laravel Addresses Simple address and contact management for Laravel with automatically geocoding to add longitude and latitude. Installation Require t

Chantouch Sek 2 Apr 4, 2022
Otpify is a Laravel package that provides a simple and elegant way to generate and validate one time passwords.

Laravel Otpify ?? Introduction Otpify is a Laravel package that provides a simple and elegant way to generate and validate one time passwords. Install

Prasanth Jayakumar 2 Sep 2, 2022
A simple job posting application using PHP with an Admin Panel. Register, Login and create the job in apnel. The job gets posted on index page.

Jobee A simple job posting application using PHP with an Admin Panel. Register, Login and create the job in apnel. The job gets posted on index page.

Fahad Makhdoomi 2 Aug 27, 2022
A simple pure PHP RADIUS client supporting Standard and Vendor-Specific Attributes in single file

BlockBox-Radius A simple pure PHP RADIUS client supporting Standard and Vendor-Specific Attributes in single file Author: Daren Yeh [email protected]

null 2 Oct 2, 2022
Laravel SEO - This is a simple and extensible package for improving SEO via meta tags, such as OpenGraph tags.

This is a simple and extensible package for improving SEO via meta tags, such as OpenGraph tags.

ARCHTECH 191 Dec 30, 2022