PHP Custom Stream Wrappers

Part of the strength of PHP's stream wrappers is the ability to add our own stream wrappers to the list of available wrappers. We can therefore natively open any type of resource just by registering a stream wrapper and then using the normal fopen() functions.

The custom stream wrapper functionality is made possible through a few functions built into PHP.

stream_get_wrappers()

The first of these is functions is stream_get_wrappers(), which returns an array of the stream wrappers available on our system. Here is an example of this in use.

php -r "print_r(stream_get_wrappers());"
Array
(
    [0] => https
    [1] => ftps
    [2] => compress.zlib
    [3] => compress.bzip2
    [4] => php
    [5] => file
    [6] => glob
    [7] => data
    [8] => http
    [9] => ftp
    [10] => phar
    [11] => zip
)

We can use any of these stream types to create resources and then work with them.

stream_wrapper_unregister()

We can unregister any of the existing streams by using the stream_wrapper_unregister() function. Using this function we can actually break PHP by removing the ability to open http addresses as streams. The following code checks for the existence of the http stream, and then removes it.

$wrapperExists = in_array("http", stream_get_wrappers());
if ($wrapperExists) {
    stream_wrapper_unregister("http");
}

Now, if we try to use the http stream through the fopen() function like this.

$stream = fopen('http://www.example.com/', 'r');
echo fread($stream, 50);

We get the following error.

PHP Warning:  fopen(): Unable to find the wrapper "http" - did you forget to enable it when you configured PHP?

This doesn't seem inherently useful, but it will come in handy when adding our own custom stream wrappers.

stream_wrapper_register()

Finally, we have the stream_wrapper_register(), with which we can register a custom stream swapper class. First though, we need to create a class that we can register as a stream wrapper. This class needs to implement a few core methods. The very minimum class you need to create should implement the following interface.

interface StreamWrapper {
    /**
     * This method is called immediately after the wrapper is initialized (f.e. by fopen() and file_get_contents()).
     *
     * @param string $path
     *   Specifies the URL that was passed to the original function.
     * @param string $mode
     *   The mode used to open the file, as detailed for fopen().
     * @param int $options
     *   Holds additional flags set by the streams API. It can hold one or more of the following
     *   values OR'd together.
     * @param string $opened_path
     *   If the path is opened successfully, and STREAM_USE_PATH is set in options, opened_path
     *   should be set to the full path of the file/resource that was actually opened.
     *
     * @return bool
     *   Returns TRUE on success or FALSE on failure.
     */
    public function stream_open(string $path, string $mode, int $options, string $opened_path = NULL): bool;

    /**
     * This method is called in response to fclose().
     *
     * No value is returned.
     */
    public function stream_close(): void;

    /**
     * This method is called in response to fread() and fgets().
     *
     * @param int $count
     *   How many bytes of data from the current position should be returned.
     *
     * @return string
     *   If there are less than count bytes available, return as many as are available. If no
     *   more data is available, return either FALSE or an empty string.
     */
    public function stream_read(int $count): string;

    /**
     * This method is called in response to feof().
     *
     * @return bool
     *   Should return TRUE if the read/write position is at the end of the stream and if no
     *   more data is available to be read, or FALSE otherwise.
     */
    public function stream_eof(): bool;

    /**
     * Seeks to specific location in a stream.
     *
     * @param int $offset
     *   The stream offset to seek to.
     * @param int $whence
     *   Possible values:
     *     - SEEK_SET - Set position equal to offset bytes.
     *     - SEEK_CUR - Set position to current location plus offset.
     *     - SEEK_END - Set position to end-of-file plus offset.
     *
     * @return bool
     *   Return TRUE if the position was updated, FALSE otherwise.
     */
    public function stream_seek(int $offset, int $whence = SEEK_SET): bool;

    /**
     * Retrieve information about a file resource.
     *
     * @return array
     *   See stat().
     */
    public function stream_stat(): array;
}

There are a few other methods available in this class. For a full list see the PHP documentation on the stream wrapper class.

With that interface we can create a simple class.

There were a few ideas that came to mind when thinking about what kind of stream wrapper class to create. Since we removed the ability to use the http stream in the last example, I thought it would be a good idea to add back in a mock http stream class.

The following class implements the StreamWrapper interface in order to create a mock http wrapper. All this class does is return a very simple html string when read. Note that the stream_read() method does more than just return a string. It will return a string the first time around and then return a blank string every time after this. The PHP documentation for stream_read() states that some functions (like file_get_contents()) will continuously loop and call stream_read() until it receives an empty string so I have added some logic to handle this.

class MockHttpStreamWrapper implements StreamWrapper
{
    protected $streamRead;

    public function stream_open(string $path, string $mode, int $options, string $opened_path = NULL): bool
    {
        // Imagine that we open a stream here.
        return true;
    }

    public function stream_close(): void
    {
        // Imagine the stream being closed here.
    }

    public function stream_read(int $count): string
    {
        if ($this->streamRead == true) {
            // If we have read the stream then return a string.
            return '';
        }

        // Set the fact that we have read the stream.
        $this->streamRead = true;

        // Return a HTML string.
        return '<body><p>Hello World</p></body>';
    }

    public function stream_eof(): bool
    {
        // Always return true.
        return true;
    }

    function stream_seek(int $offset, int $whence = SEEK_SET): bool
    {
        return false;
    }

    public function stream_stat(): array
    {
        return [];
    }
}

This class can then be used as a http stream wrapper by using the stream_wrapper_register() function.

stream_wrapper_register('http', 'MockHttpStreamWrapper', STREAM_IS_URL);

We pass in 'http' as the stream type and the class name MockHttpStreamWrapper as the class we defined above. The third parameter is any additional flags, which should be set to STREAM_IS_URL if protocol is a URL protocol (which we are setting here). The default here is 0, which is a local stream.

Note that we first need to unregister the http stream wrapper before we can register our own one or PHP will throw an error.

$existed = in_array("http", stream_get_wrappers());
if ($existed) {
    // Unregister the http stream wrapper.
    stream_wrapper_unregister("http");
}

// Register our custom stream wrapper.
stream_wrapper_register('http', 'MockHttpStreamWrapper', STREAM_IS_URL);

// Use the new custom stream wrapper.
$stream = fopen('http://www.example.com/', 'r');
while (false !== ($line = fgets($stream))) {
  echo $line;
}
fclose($stream);

This code outputs the following.

<body><p>Hello World</p></body>%

This doesn't seem that useful at face value. However, what we have done here in intercept any communication through http using fopen() or file_get_contents(). This is useful if you are using fopen() to interact with an API and want to create a mock for testing purposes.

There are a few examples out there that show this method being used to interface with gluster file systems, S3 resources and all sorts of different ways of interacting with data. Stream wrappers are used in Drupal 8 to provide an interface to the public and private file systems. This means that we can use fopen('public://file.txt') to open a file in the public file directory without having to get the developer to include a bunch of boiler plate code to translate the scheme to a location.

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
7 + 0 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.