What is a bucket brigade?

Ah, welcome to the least documented parts of the PHP manual! [I opened a bug report about it; maybe this answer will be helpful for documenting it: https://bugs.php.net/bug.php?id=69966]

The bucket brigade

To start with your initial question, the bucket brigade is just a name to the resource named userfilter.bucket brigade.

You are passed two different brigades in as first and second parameters to php_user_filter::filter(). The first brigade is the input buckets you read from, the second brigade is initially empty; you write to it.

Regarding your update about the data structure… It's really just a doubly linked list with strings basically. But it may well be that the name was stolen from there ;-)

stream_bucket_prepend() / stream_bucket_append()

stream_bucket_prepend(resource $brigade, stdClass $bucket): null
stream_bucket_append(resource $brigade, stdClass $bucket): null

The expected $brigade is the output brigade aka the second parameter on php_user_filter::filter().

The $bucket is a stdClass object like it is returned by stream_bucket_make_writable() or stream_bucket_new().

These two functions just prepend or append the passed bucket to the brigade.

stream_bucket_new()

To demystify this function, analyze first what it's function signature is:

stream_bucket_new(resource $stream, string $buffer): stdClass

First argument is the $stream you're writing this bucket to. Second is the $buffer this new bucket will contain.

[I'd like to note here that the $stream parameter actually is not very significant; it's just used to check whether we need to allocate memory persistently so that it survives through requests. I just suppose that you can make PHP nicely segfault by passing a persistent stream in here, when operating on a non-persistent filter...]

There is now an userfilter.bucket resource created which is assigned to a property of a (stdClass) object named bucket. That object has also two other properties: data and datalen, which contain the buffer and the buffer size of this bucket.

It will return you a stdClass which you can pass in to stream_bucket_prepend() and stream_bucket_append().

stream_bucket_make_writable()

stream_bucket_make_writeable(resource $brigade): stdClass|null

It shifts the first bucket from the $brigade and returns it. If the $brigade was emptied, it returns null.

Further notes

When php_user_filter::filter() is called, the $stream property on the object filter() is called on will be set to the stream we're currently working on. That's also the stream you need to pass to stream_bucket_new() when calling it. (The $stream property will be unset again after the call. You can't reuse it in e.g. php_user_filter::onClose()).

Also note that even when you're returned a $datalen property, you do not need to set that property in case you change $data property before passing it to stream_bucket_prepend() or stream_bucket_append().

The implementation requires you (well, it expects that or will throw a warning) that you read all the data from the $in bucket before returning.

There is another case of the documentation lying to us: in php_user_filter::onCreate(), the $stream property is not set. It will only be set during filter() method call.

Generally, don't use filters with non-blocking streams. I tried that once and it went horribly wrong … And it's not likely that's ever going to be fixed...

Sum up (examples)

Let's start with the simplest case: writing back what we got in.

class simple_filter extends php_user_filter {
    function filter($in, $out, &$consumed, $closing) {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $consumed += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

stream_filter_register("simple", "simple_filter")

All what happens here is getting buckets from $in bucket brigade and putting it back into $out bucket brigade.

Okay, now try to manipulate our input.

class reverse_filter extends php_user_filter {
    function filter($in, $out, &$consumed, $closing) {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $consumed += $bucket->datalen;
            $bucket->data = strrev($bucket->data);
            stream_bucket_prepend($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

stream_filter_register("reverse", "reverse_filter")

Now we registered the reverse:// protocol, which reverses your string (each write is being reversed on it's own here; write order is still preserved). So, we obviously now need to manipulate the bucket data and prepend it here.

Now, what's the use case for stream_bucket_new()? Usually you can just append to $bucket->data; yes, you even can concatenate all the data into the first bucket, but when flush()'ing it might be possible that nothing is in bucket brigade and you want to send a last bucket, then you need it.

class append_filter extends php_user_filter {
    public $stream;

    function filter($in, $out, &$consumed, $closing) {
        while ($bucket = stream_bucket_make_writeable($in)) {
            $consumed += $bucket->datalen;
            stream_bucket_append($out, $bucket);
        }
        // always append a terminating \n
        if ($closing) {
            $bucket = stream_bucket_new($this->stream, "\n");
            stream_bucket_append($out, $bucket);
        }
        return PSFS_PASS_ON;
    }
}

stream_filter_register("append", "append_filter")

With that (and the existing documentation about php_user_filter class), one should be able to do all sorts of magic userland stream filtering by combining all these powerful possibilities into even stronger code.