Good Practices: use stream in PHP

Coding (Php 7.x)


Transfer data the right way using stream, the hidden gem of PHP
 
/img/blog/Good-practices-stream-wrapper-filter-php.jpg

Introduction

If you travel often you surely have been spending more time than you would like at airports,


I love travelling but all that waiting, then the checking,  then waiting again just bother me.

 

In one of my last trip, for example, I had to wait for a few hours at my destination just to have my luggage back.

 

Minutes and minutes next to an empty conveyer belt with the hope that the next suitcase spit by the machine would be mine.

 

That reminded me of a feature that works amazingly well in PHP.

 

Streams!

 

If you have ever worked on files and archives with your PHP scripts you have used streams. 

 

They are uncommon but not that complicated.

 

Today you are here to get to know them.
 

 

The series

This is the PHP good practices series, 

 

In this series, we are exploring what are the best practices a web developer must take care of when creating or managing PHP code.

 

If you missed the previous episodes follow the links below
 

  1. Sanitize, validate and escape
  2. Security and managing passwords
  3. Handling error and exceptions
  4. Dates and Time
  5. Streams

 

What are streams

Streams were released for the first time in PHP 4.3.

 

But, maybe because of their niche, or just the lack of content online, they are rarely known.

 

In PHP 7, Zend certification study guide Andrew Beak describes them as a “conveyer belt of things that come to your one by one”.

 

The good news is that using PHP you do not need to wait for the things to arrive you can just go and get them.

 

So, what are streams?

 

A stream is a transfer of data between to places.

 

The places can be a file, a ZIP archive,  a connection even a process through the command line.

 

PHP streams have functions that help developers manage different resources,

 

I am sure you have already seen the functions fopen(), fwrite(), fgets() or file_get_contents(), well streams provides the implementation that works in the background of these functions.
 

 

The Wrappers

I am sure you understand the difference between open a file or a web page, they both have content but they are two completely different types of streamable data, hence they require different protocols.

 

For instance, we can open a web page by connecting with remote web servers using HTTP, HTTPS or SSL or read a ZIP or TAR archive.

 

These protocols are called stream wrappers. They provide a unique interface that encapsulates all these differences.

 

Every stream is formed from a scheme and a target, here is the format:
scheme://target

Look familiar?

If not just look up at the address bar of your browser.


PHP include a list of wrapper that are built-in in the language.

  • file://  Accessing local filesystem
  • http://  Accessing HTTP(s) URLs
  • ftp://  Accessing FTP(s) URLs
  • php://  Accessing various I/O streams
  • zlib://  Compression Streams
  • data://  Data (RFC 2397)
  • glob://  Find pathnames matching pattern
  • phar://  PHP Archive
  • ssh2:// Secure Shell 2
  • rar://  RAR
  • ogg://  Audio streams
  • expect://  Process Interaction Streams

In our case php:// can access the php://stdin, php://stdout, php://stderr, php://input, php://output, php://fd, php://memory, php://temp and php://filter.

 

That is all nice so far but you may be wondering what the real work use of these streams.

 

Let’s make an example.

 

How do you read the body of a REST API where the user update some details somewhere on the web?

 

That a PUT request and there is not $_PUT[variable’] in PHP;

 

The answer is through a stream

 

// the line reads the content 
$input = file_get_contents('php://input');
// then we parse the input into an array of parameters
parse_str($input, $props);

 

In a different example we can use the file wrapper and read a file:

 

// file /etc/host is opened are read
$handle = fopen("file:///etc/host", r);
// If the file in not finished output the line from file pointer
while (feof($handle) !== true) {
    echo fgets($handle);
}
// Eventually the file get closed for security reasons
fclose($handle);

 

If the snippets above look too strange you may want to stop for a moment and hand over the article the basics of PHP

 

A common false believe that is that PHP functions like fopen(), fgets(), fclose() are for filesystem only,

 

These functions work perfectly fine with all wrappers that support them.

 

We can use fopen() on files, ZIP archive and Dropbox (with the Dropbox wrapper) and Amazon S3 (with its own wrapper).
 

 

Create your custom wrapper

 

If your script needs special requirements PHP allows to create a custom wrapper by yourself.

 

In the snippet below we are going to create a stream wrapper that calls a callback function for reads:

 

/ The following class is the wrapper, it has several parameters and the getContext() stream_open(), stream_read() and stream_eof() methods
class CallbackUrl
{
    const WRAPPER_NAME = 'callback';

    public $context;
    private $_cb;
    private $_eof = false;

    private static $_isRegistered = false;

    public static function getContext($cb)
    {
        if (!self::$_isRegistered) {
            stream_wrapper_register(self::WRAPPER_NAME, get_class());
            self::$_isRegistered = true;
        }
        if (!is_callable($cb)) return false;
        return stream_context_create(array(self::WRAPPER_NAME => array('cb' => $cb)));
    }

    public function stream_open($path, $mode, $options, &$opened_path)
    {
        if (!preg_match('/^r[bt]?$/', $mode) || !$this->context) return false;
        $opt = stream_context_get_options($this->context);
        if (!is_array($opt[self::WRAPPER_NAME]) ||
            !isset($opt[self::WRAPPER_NAME]['cb']) ||
            !is_callable($opt[self::WRAPPER_NAME]['cb'])) return false;
        $this->_cb = $opt[self::WRAPPER_NAME]['cb'];
        return true;
    }

    public function stream_read($count)
    {
        if ($this->_eof || !$count) return '';
        if (($string = call_user_func($this->_cb, $count)) == '') $this->_eof = true;
        return $string;
    }

    public function stream_eof()
    {
        return $this->_eof;
    }
}

// Here is the class that will contain the text object that need to be instanciated
class Text {
    private $_string;
    public function __construct($string)
    {
        $this->$_string = $string;
    }
    public function read($count) {
        return fread($this->$_string, $count);
    }
}


// We create a new Text object and open it using our brand-new wrapper CallbackUrl eventually will loop until the file ends
$text = new Text(fopen('/etc/services', 'r'));
$handle = fopen('callback://', 'r', false, CallbackUrl::getContext(array($text, 'read')));
while(($row = fread($handle, 128)) != '') {
    print $row;
}

 

You can find few other very well explained examples in Matt Allan's post a full stack developer based in Asheville, NC

 

 

Stream Filters

 

According to a lot of developers that use stream often, the real power of this PHP feature is hidden behind the filters.

 

Stream filters enable to filter and transform data that is currently transiting.

 

Imagine you can transform and output all the strings in a file in uppercase, while you are reading the file.

 

There are 2 ways to use stream filters

  • Using stream_filter_append($stream, $filtername, $read_write)
  • Using php://filter stream wrapper

 

stream_filter_append()

 

Let’s have a look at an example and I’ll comment it after

 

$handle = fopen('file.txt', 'rb');
stream_filter_append($handle, 'string.toupper');
while(feof($handle) !== true) {
    echo fgets($handle);
}
fclose($handle);

 

In the first line,

 

we open the file.txt which is supposed to contain string in lowercase that we want to transform in uppercase.

 

We then, attach (append) the handle of the file to the stream filter string.toupper

 

Now PHP knows that when we loop through the haldle the characters need to be shown as uppercase.

 

Just like magic!
 

php://filter

 

Sometimes you are going to need to use functions such as file() or fpassthru() that do not give you the chance to attach filters after the functions have been called.

 

Which means that you cannot append a stream filter.

 

In this case, you use php://filter and use it straight when you open the file.

 

Snippet is following
 

$handle = fopen('php://filter/read=string.toupper/resource=file.txt', 'rb');
while(feof($handle) !== true) {
    echo fgets($handle); 
}
fclose($handle);

 

Again we attached the filter string.toupper to the text.txt file on reading but this time, we did it on the opening command;

 

Be aware of the format we are using here: filter/read=/resource=://
 

Create a custom stream filters

 

Stream filters are a powerful tool, however, the built-in filter provided by PHP is rather limited, to offset this limitation PHP lets us create custom stream filter,

 

Actually the custom filters are the primary reason we use filters.


Custom stream filters are just classes that extend the behaviour of php_user_filter()

 

That’s all.

 

This class must have 3 methods implemented.

 

The first method and the one we are going to look just below is the filter(), then we have the onCreate and the onClose.

 

Before dive in and create a stream on our own clarification.

 

PHP streams divide data into sections of several bytes each, those division are commonly called buckets.

 

Each stream filter receives and manages one or more of these buckets at a time.

 

Let’s create a custom stream filter together.

 

The process requires 3 steps

  1. Create a filter that implements php_user_filter()
  2. Register this new filter
  3. Use the brand new filter in an actual script

 

Create a filter 

 

In these examples, we are going to create a filter that edits dirty or spamming words from text files.

 

As said before we need to create a class that needs to implement php_user_filter than implement the filter() method.
 

class DirtyWordsFilter extends php_user_filter
{
   public function filter($in, $out, &$consumed, $closing)
   {
       $words = ['grime', 'dirt', 'grease'];
       $wordData = [];
       
       foreach ($words as $word) {
          $replacement = array_fill(0, mb_strlen($word), '*');
          $wordData[$word] = implode('', $replacement);
       }
      
       $bad = array_keys($wordData);
       $good = array_values($wordData);
      
       // Iterate each bucket
       while ($bucket = stream_bucket_make_writeable($in)) {
          // Censor dirty words
          $bucket->data = str_replace($bad, $good, $bucket->data);
         
          // increment total data consumed
          $consumed += $bucket->datalen;
         
          // send bucket to downstream brigade
          stream_bucket_append($out, $bucket);
     }

    // Return the code that indicates that the userspace filter returned buckets in $out
     return PSFS_PASS_ON;
   }
}

 

Now that the filter is ready we need to register it, that quite simple.

 

We just need to use the function stream_filter_register() and pass the filter name that identifies our new filter and the name of the class.
 

stream_filter_register('dirty_words_filter', 'DirtyWordsFilter');

 

Here is the page from the official PHP manual

 

We are ready to use our filter as we did in the above examples:

 

$handle = fopen('file.txt', 'rb');
stream_filter_append($handle, 'dirty_words_filter');
while(feof($handle) !== true) {
    echo fgets($handle);
}
fclose($handle);

 

That’s it, we are safe and not going to have any dirty words anymore.

 

Stream Contexts

 

The last section about this topic is about contexts,

 

Consider stream contexts like a wrapper for a set of options that can modify the behaviour of a stream.

 

To create a stream context you’ll need to use the stream_context_create() function.

 

It takes two parameters, both of them are optional associative arrays.

 

Let’s use a stream context to something strange like sending an HTTP POST request using file_get_contents()
 

$request = '{"username":"nico"}';
$context = stream_context_create([
  'http' => [
     'method' => 'POST',
     'header' => "Content-Type: application/json;charset=utf-8;\r\n" . "Content-Length: " . mb_strlen($requestBody),
     'content' => $requestBody
  ]
]);
$response = file_get_contents('https://mywebsite.com/users', false, $content);

 

The stream context is an associative array in which the top key is the stream wrapper name.

 

Head to the manual for more info about stream context

 

Conclusion

Sometimes travelling can be stressful and you may need to get to work in order to relax a bit.

 

Now if you need to use files in your script or manage archives you know,

 

PHP streams help you in that.

 

There aren’t a lot of occasions in which you can use them and, actually, their usability is quite hidden to most web developers.

 

But they are really easy to use and in my opinion, this content can help you discover this little gem of the PHP language.

 
 
If you like this content and you are hungry for some more join the Facebook's community in which we share info and news just like this one!

Other posts that might interest you

Coding (Php 7.x) Sep 25, 2019

Good Practices: Dates, Time & Time zones in PHP

Check the ebook See details
Coding (Php 7.x) Oct 11, 2019

How‌ ‌to‌ ‌become‌ the ‌best‌ ‌programmer‌ ‌in‌ ‌the‌ ‌world‌ (My journey)

See details
Coding (Php 7.x) Oct 19, 2019

The 5 SOLID principles with examples [in PHP]

See details
Get my free books' review to improve your skill now!
I'll do myself