Open-source project
clue/stream-filter avatar
clue/stream-filter

clue/stream-filter: PHP stream filters without the bucket brigade

A simple and modern approach to stream filtering in PHP

1,703 stars16 forksPHPMIT

At a glance

What is it?
This library wraps PHP's underused user filter API in four functions so that on-the-fly stream transformation takes a callable instead of a class with bucket brigade boilerplate. The catch is PHP's own: attaching a filter to a socket makes it unusable with stream_select.
Who is it for?
Adopt clue/stream-filter when you need on-the-fly transformation of file or stream data in PHP and the alternative is hand writing a user filter class, because four functions replace the whole bucket brigade pattern. Do not attach filters to sockets you also pass to stream_select, since PHP can no longer tell when a filtered stream is ready, and use fun() with an unfiltered stream when you need both.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 111 days ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What PHP's filter API costs you before this library

PHP has had stream filters for a long time, and they are the mechanism behind reading a gzip'ed input, transcoding from ISO-8859-1 to UTF-8, and writing a bzip output file. The README does not dispute the power. It disputes the ergonomics: the API is described as difficult to work with and the documentation as subpar, and the consequence it names is specific, that these features are often neglected.

The cost of the native API is a class. To transform data on the fly you register a filter with a class implementing the user filter interface and supply an onFilter method, and PHP's own design requires a bucket brigade to link buckets onto the stream. The README links both the manual page and a Stack Overflow question titled what is a bucket brigade as its evidence.

clue/stream-filter replaces that with a callable. The entire surface is four functions in the `Clue\StreamFilter` namespace: `append()`, `prepend()`, `fun()` and `remove()`. The README describes it as a lightweight, SOLID design that provides a thin abstraction which is just good enough and does not get in your way, with custom filters requiring trivial effort.

The intended user is a PHP developer who knows this capability exists, has avoided it because of the ceremony, and wants it for the four common cases: chunk transformation, using a built-in filter, restricting the callback to reads or writes, and getting a flush call at end of stream.

append() takes a callable that returns a transformed chunk

The core function takes three arguments: the stream, the callback, and an optional integer direction that defaults to `STREAM_FILTER_ALL`. It returns a stream filter resource.

The stream is any valid stream resource, such as one opened with `fopen('demo.txt', 'w+')`. The callback receives an individual chunk of data and returns the updated chunk. Filters attach to streams as an ordered list, and `append()` adds to the end of that list, while `prepend()` adds to the start and behaves identically otherwise.

Because the second argument is any callable, a named PHP function works directly:

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

fwrite($stream, 'hello');

That writes "HELLO" to the underlying stream. No class, no bucket brigade, no registration call. The examples directory holds the same idea as three runnable scripts, `base64_encode.php`, `base64_decode.php` and `uppercase.php`.

If the filter cannot be added, the function throws an Exception. That is the failure mode to handle at the call site.

If a filter cannot be added, that exception is your signal that the stream or the filter is invalid, and it is one of the few places the library surfaces a problem loudly.

The null chunk is the flush signal, and PHP versions before 5.4 lose it

There is one piece of genuine protocol in the callback signature. If your callback can be invoked with no parameters, that invocation is used once at the end, when the filter is flushed:

php
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);

The default of `null` is what makes the callback dual purpose, and returning something on the flush call is what lets you append a terminator or write a trailing header. PHP calls it when the stream closes.

The version caveat is stated explicitly: legacy PHP versions below 5.4 do not support passing additional data from the end signal handler if the stream is being closed. On those versions the flush call still happens but your return value is not usable, which matters if you are writing a footer or a length prefix.

The second documented behaviour of this flush is that if your callback throws an Exception, the filter process is aborted, and the Exception is transformed into a PHP warning to play nicely with PHP's stream handling. The observable effect is an E_USER_WARNING reading "Error invoking filter: Unexpected chunk" raised on the fwrite call.

That transformation is a deliberate design choice with a real cost. If your filter must fail loudly, do not rely on throwing.

stream_select and filtered streams do not mix

The most important limitation in the README is a warning rather than a feature, and it deserves to be read before any of the examples.

Once a filter has been added to a stream, that stream can no longer be passed to `stream_select()` or its family. PHP emits a warning along the lines of `stream_select(): cannot cast a filtered stream on this system`. The reason given is a limitation of PHP's stream filter support: the runtime can no longer reliably tell when the underlying stream resource is actually ready.

That is fatal for the most common streaming use case. If you are reading from a socket, a pipe or a process, waiting for readiness with stream_select is how you avoid blocking, and attaching a filter destroys that ability for that stream.

The README gives the workaround, and it is a specific one: call `stream_select()` on the unfiltered stream, then pass the unfiltered data through the `fun()` function yourself. That inverts the model. Instead of the stream transforming its own bytes, you select on the raw resource and apply the transformation at the point of reading.

So the design has two modes. Attached filters for file and buffer style work, where you write and read through the resource and never need readiness notification. Manual `fun()` application for anything that must be selectable, including sockets and pipes.

The third argument on `append()` narrows when the callback fires at all:

php
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);

Passing `STREAM_FILTER_WRITE` means the callback runs on writes only, `STREAM_FILTER_READ` on reads only, and the default `STREAM_FILTER_ALL` on both.

fun() and remove() close the loop

`fun()` is the bridge between the two modes above. It takes a built-in filter name and an optional parameters argument, and returns a callable.

That callable wraps the given built-in filter, including a built-in gzip or bzip filter. The returned value is a plain callable, which means it drops into both `append()` and `prepend()` unchanged, and it can equally be called yourself on data you read from an unfiltered stream.

That dual use is the point. The same expression works as an attached filter for file work and as a manual transform for socket work, which is what makes the stream_select workaround practical rather than theoretical.

`remove()` takes the filter resource returned by `append()` or `prepend()` and detaches it. Since those functions return a filter resource specifically so it can be handed back, the intended lifecycle is attach, use, detach, rather than filtering for the lifetime of the stream. The README describes the remove function only in passing, as the destination for the returned value, so read its signature in the source at the version you pin.

Three import styles are documented, which matters for older codebases. Fully qualified calls as `Clue\StreamFilter\append(...)`, the PHP 5.6+ function import `use function Clue\StreamFilter\append;`, or a namespace alias `use Clue\StreamFilter as Filter;` followed by `Filter\append(...)`.

Maintenance, licence and where this fits

This is a small library with a specific audience, and both facts should inform the adoption decision.

The licence is MIT. There is no copyleft obligation on your application and no attribution burden beyond retaining the notice, which matters little at this size but matters for a dependency in a widely distributed package.

On activity, the repository is not archived and the last push was on 2026-07-03. The release history is much older: v1.7.0 from 2023-12-20, v1.6.0 from 2022-02-21 and v1.5.0 from 2020-10-02. The default branch is `1.x`. That pattern, recent commits on an existing stable line with no new feature releases, reads as maintenance of a finished component rather than active development of a new one. For a library whose feature set is four functions, that is exactly the right cadence, and it is also a low risk dependency since the API surface is unlikely to change.

The alternatives are worth naming concretely. One is writing the user filter class yourself, which is what most code does today and what this library exists to avoid. Two is not using filters at all and reading the whole stream into memory with `stream_get_contents()`, then transforming the string. Three is a different extension entirely, such as a memory limit friendly parser for the specific format you are handling, which beats a generic filter when you only ever read one format.

Choose this library when the transformation is line oriented or chunk oriented and the data can be processed as it flows. Choose the memory approach when the stream is small or you need the data whole anyway. Choose a format parser when you are reading one format rather than transforming bytes.

The repository also carries a `CHANGELOG.md`, a `phpunit.xml.dist` alongside a `phpunit.xml.legacy`, and a CI workflow, and the README states the project targets 100% code coverage.

Editorial conclusion

Adopt clue/stream-filter when you need on-the-fly transformation of file or stream data in PHP and the alternative is hand writing a user filter class, because four functions replace the whole bucket brigade pattern. Do not attach filters to sockets you also pass to stream_select, since PHP can no longer tell when a filtered stream is ready, and use fun() with an unfiltered stream when you need both. Verify first that your PHP version supports the optional end-of-filter argument, because the README notes that PHP before 5.4 cannot receive additional data from the end signal when a stream is closed.

Frequently asked questions

How do I install clue/stream-filter?

The README's Install section is present but the package is distributed on Packagist as clue/stream-filter, and the repository includes a composer.json. The library requires no PHP extension or configuration once installed.

What does the callback passed to append() receive and return?

It receives an individual chunk of data and returns the updated chunk. Any callable works, including a native function such as 'strtoupper', so Clue\StreamFilter\append($stream, 'strtoupper') uppercases everything written to the stream.

Can I still use stream_select() on a stream that has a filter attached?

No. The README warns that once a filter is added, the stream can no longer be passed to stream_select() and its family, because PHP can no longer reliably tell when the underlying resource is ready. It suggests selecting on the unfiltered stream and applying fun() to the data instead.

How do I run code when a stream filter finishes?

Declare the callback parameter with a default of null. When the callback is invoked with $chunk === null that is the flush call at end of stream, which happens on fclose. PHP versions below 5.4 do not support returning additional data from that end signal.

What happens if my stream filter callback throws an exception?

The filter process is aborted and the Exception is transformed into a PHP warning to align with PHP stream handling, raising E_USER_WARNING with a message like "Error invoking filter: Unexpected chunk".

Official sources

  1. clue/stream-filter on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/clue-stream-filter.svg)](https://hysenlabs.com/projects/clue-stream-filter)

Was this useful?