For posterity, this is what I ended up whipping. The filter itself comes from php_user_filter , which is described in the documents for stream_filter_register :
class HookableFilter extends php_user_filter { public function filter($in, $out, &$consumed, $closing) { $data = ''; while ($bucket = stream_bucket_make_writeable($in)) { $consumed += $bucket->datalen; $data .= $bucket->data; stream_bucket_append($out, $bucket); } call_user_func($this->params, $data); return PSFS_PASS_ON; } }
Note that I use $this->params directly as a callback, making it really Spartan in the function (there is a reason, see below).
Registering a filter using PHP:
stream_filter_register('generic.hookable', 'HookableFilter');
Attaching a filter to a stream:
$callback = function($data) { }; stream_filter_append($stream, 'generic.hookable', STREAM_FILTER_READ, $callback); stream_filter_append($stream, 'generic.hookable', STREAM_FILTER_WRITE, $callback);
Important: As far as I could see, if you attach a filter to both channels of a duplex stream (for example, one of them was created with stream_socket_client ) so that your filter does not know which channel it works when it is called. Therefore, if you need to distinguish between incoming and outgoing data as I was, the only option is to attach a filter separately to each channel. If you do this, you will also definitely want to provide a different callback for each channel (not the same as done in the simplified example above).