phpstreamstream-wrapper

Using a custom stream wrapper as test stub for PHP's http:// stream wrapper


I'm writing a custom stream wrapper to use as a stub in unit tests for an HTTP client class that uses the built-in http:// stream wrapper.

Specifically, I need control over the value returned in the 'wrapper_data' key by calls to stream_get_meta_data on streams created by the custom stream wrapper. Unfortunately, the documentation on custom stream wrappers is woeful and the API seems unintuitive.

What method in a custom wrapper controls the the meta wrapper_data response?

Using the class at the bottom I've only been able to get the following result when I var_dump(stream_get_meta_data($stream)); on streams created with the custom wrapper ...

array(10) {
  'wrapper_data' =>
  class CustomHttpStreamWrapper#5 (3) {
    public $context =>
    resource(13) of type (stream-context)
    public $position =>
    int(0)
    public $bodyData =>
    string(14) "test body data"
  }
  ...

But I need to coax the wrapper into yielding something like the following on meta data retrieval so I can test the client class's parsing of the data returned by the real http:// stream wrapper ...

array(10) {
  'wrapper_data' => Array(
       [0] => HTTP/1.1 200 OK
       [1] => Content-Length: 438
   )
   ...

Here's the code I have currently for the custom wrapper:

class CustomHttpStreamWrapper {

    public $context;
    public $position = 0;
    public $bodyData = 'test body data';

    public function stream_open($path, $mode, $options, &$opened_path) {
        return true;
    }

    public function stream_read($count) {
        $this->position += strlen($this->bodyData);
        if ($this->position > strlen($this->bodyData)) {
            return false;
        }
        return $this->bodyData;
    }

    public function stream_eof() {
        return $this->position >= strlen($this->bodyData);
    }

    public function stream_stat() {
        return array('wrapper_data' => array('test'));
    }

    public function stream_tell() {
        return $this->position;
    }
}

Solution

  • stream_get_meta_data is implemented in ext/standard/streamfunc.c. The relevant part is

    if (stream->wrapperdata) {
        MAKE_STD_ZVAL(newval);
        MAKE_COPY_ZVAL(&stream->wrapperdata, newval);
    
        add_assoc_zval(return_value, "wrapper_data", newval);
    }
    

    i.e. whatever zval stream->wrapperdata holds is "copied" to/referenced by $retval["wrapper_data"].
    Your custom wrapper code is "handled" by user_wrapper_opener in main/streams/userspace.c. And there you have

    /* set wrapper data to be a reference to our object */
    stream->wrapperdata = us->object;
    

    us->object "is" the instance of your custom wrapper that has been instantiated for the stream. I haven't found a way to influence stream->wrapperdata from userspace scripts other than that.
    But you could implement Iterator/IteratorAggregate and/or ArrayAccess if all you need is foreach($metadata['wrapper_data'] ...) and $metadata['wrapper_data'][$i].
    E.g.

    <?php
    function test() {
        stream_wrapper_register("mock", "CustomHttpStreamWrapper") or die("Failed to register protocol");
        $fp = fopen("mock://myvar", "r+");
        $md = stream_get_meta_data($fp);
    
        echo "Iterator / IteratorAggregate\n";
        foreach($md['wrapper_data'] as $e) {
            echo $e, "\n";
        }
    
        echo "\nArrayAccess\n";
        echo $md['wrapper_data'][0], "\n";
    
        echo "\nvar_dump\n";
        echo var_dump($md['wrapper_data']);
    }
    
    class CustomHttpStreamWrapper implements IteratorAggregate, ArrayAccess  {
        public $context;
        public $position = 0;
        public $bodyData = 'test body data';
    
        protected $foo = array('HTTP/1.1 200 OK', 'Content-Length: 438', 'foo: bar', 'ham: eggs');
        /* IteratorAggregate */
        public function getIterator() {
            return new ArrayIterator($this->foo);
        }
        /* ArrayAccess */
        public function offsetExists($offset) { return array_key_exists($offset, $this->foo); }
        public function offsetGet($offset ) { return $this->foo[$offset]; }
        public function offsetSet($offset, $value) { $this->foo[$offset] = $value; }
        public function offsetUnset($offset) { unset($this->foo[$offset]); }
    
        /* StreamWrapper */
        public function stream_open($path, $mode, $options, &$opened_path) {
            return true;
        }
    
        public function stream_read($count) {
            $this->position += strlen($this->bodyData);
            if ($this->position > strlen($this->bodyData)) {
                return false;
            }
            return $this->bodyData;
        }
    
        public function stream_eof() {
            return $this->position >= strlen($this->bodyData);
        }
    
        public function stream_stat() {
            return array('wrapper_data' => array('test'));
        }
    
        public function stream_tell() {
            return $this->position;
        }
    }
    
    test();
    

    prints

    Iterator / IteratorAggregate
    HTTP/1.1 200 OK
    Content-Length: 438
    foo: bar
    ham: eggs
    
    ArrayAccess
    HTTP/1.1 200 OK
    
    var_dump
    object(CustomHttpStreamWrapper)#1 (4) {
      ["context"]=>
      resource(5) of type (stream-context)
      ["position"]=>
      int(0)
      ["bodyData"]=>
      string(14) "test body data"
      ["foo":protected]=>
      array(4) {
        [0]=>
        string(15) "HTTP/1.1 200 OK"
        [1]=>
        string(19) "Content-Length: 438"
        [2]=>
        string(8) "foo: bar"
        [3]=>
        string(9) "ham: eggs"
      }
    }