PHP Temporary Streams
-
It’s been a while since David Sklar called out to let a thousand string concatenations bloom. That discussion produced some entertaining suggestions for putting strings together such as using
preg_replace
and calling out to MySQL withSELECT CONCAT
.Here’s an approach that uses filesystem functions. When combined with some lesser-known PHP streams functionality, it has several practical applications.
Opening Files for Reading and Writing
There are several modes for opening a file that will allow you to seek to arbitrary positions in the file and read or write at those positions. One of the most frequently used of these modes is
w+
, which thetmpfile
function uses automatically.We can repeatedly write, then rewind the pointer and read:
$f = tmpfile(); fwrite($f, 'foo'); fwrite($f, 'bar'); rewind($f); $contents = stream_get_contents($f); //=> "foobar" fclose($f);
When writing to the filesystem, the above provides yet another inefficient solution to David’s exercise. Now let’s take it a bit further to see how this can be useful.
In-Memory Streams
PHP 5.1 introduced two new in-memory streams:
php://memory
andphp://temp
. Thephp://memory
stream operates entirely in memory. Thephp://temp
stream operates in memory until it reaches a given size, then transparently switches to the filesystem.We can modify the above example to use the
php://memory
stream instead of hitting the filesystem:$f = fopen('php://memory', 'w+'); fwrite($f, 'foo'); fwrite($f, 'bar'); rewind($f); $contents = stream_get_contents($f); //=> "foobar" fclose($f);
Putting a string inside a fast temporary stream can be very useful. For example, we can then attach filters to that stream.
Testing
Temporary streams are also handy for testing. There are some rather elaborate virtual file system libraries out there but many times a stream is all you need.
Zend_Log has a log handler for streams that accepts either a filename or a stream resource. We can configure it with a
php://memory
stream for testing:$f = fopen('php://memory', 'w+'); $writer = new Zend_Log_Writer_Stream($f); $logger = new Zend_Log($writer);
Assuming your well-designed application has a convenient injection point for the logger instance, your test can pass it in before your test activates some action which should result in logging:
$logger->crit('critical message worth testing');
When the action has completed, the test can rewind
$f
and use it as a test spy.rewind($f); $contents = stream_get_contents($f); $this->assertRegExp('/message worth testing/i', $contents); // PHPUnit
Not surprisingly, my unit tests for Zend_Log use this same technique.
Application Usage
Not long ago, we built a custom document storage system for a client. One of its more interesting features is that it integrates with an internet fax service so users can select documents in the system and then fax them. For each fax, the software automatically generates a cover page.
To make the cover page, I first made a nice template using a drawing tool and then saved it in PDF format. I then used Zend_Pdf to write over the template with dynamic content. I first demonstrated this technique in this article.
Since the cover page is only used once (during transmission) and easy to regenerate, I don’t save the output to the filesystem. Instead, I create a
php://temp
stream. The instance methodZend_Pdf->render()
writes the PDF output only to that stream, which is then rewound. Functions likestream_copy_to_stream
orfpassthru
can then be used to send the final output where it needs to go, and the whole process normally never needs to use the disk.