Transfer data the right way using stream, the hidden gem of PHP
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
Sanitize, validate and escape
Security and managing passwords
Handling error and exceptions
Dates and Time
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);
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;
}
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
- Create a filter that implements php_user_filter()
- Register this new filter
- 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
If you discovered something new, learning more is as easy as tapping into the image below
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.
Top comments (0)