PHP Temporary Streams

  • Posted by Mike Naberezny in PHP,Testing

    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 with SELECT 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 the tmpfile 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 and php://temp. The php://memory stream operates entirely in memory. The php://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 method Zend_Pdf->render() writes the PDF output only to that stream, which is then rewound. Functions like stream_copy_to_stream or fpassthru 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.

6 comments

  • comment by daniel 18 Oct 08

    Interesting read. Could you tell a bit more about this fax integration? Do you simply send a pdf file to the machine on a network? And if so, what tools you’re using for that (some native OS commands or pure php?)? Thanks

  • comment by Mike Naberezny 18 Oct 08

    We used interfax.net. It provides a SOAP-based web service where you can send documents for faxing and then later poll their transmission status.

  • comment by Jared 18 Oct 08

    I’ve tried to use the memory streams in the past, together with the compression filters. But sadly it doesn’t work.

    Think the compression filters require the fclose() to finalise things, by which point you’ve lost the memory stream.

  • comment by Dave Marshall 19 Oct 08

    I’ve been using the temporary streams for a while now and particularly for testing like you pointed out, but I found something a little strange when doing so. I found that you could open php://memory in read only mode, but still are able to write to it, which scuppered some testing I was doing on my own logging class. I reported a bug, but nobody seemed interested, probably as it’s a strange use-case.

    http://bugs.php.net/bug.php?id=44818

  • comment by Jared 20 Oct 08

    Ah, this post got me thinking about it again and discovered how to get compression filters working.
    Have to remove the filter from the stream before try and read the compressed contents.

  • comment by mvug 5 Feb 09

    I get sometimes (with zend_pdf) the folowing error:
    Fatal error: Allowed memory size of 33554432 bytes exhausted (tried to allocate 1572864 bytes)

    Is the temporary streams (php://temp) a way to fix this? Does anybody know if it’s possible for zend_pdf to use a tempfile instead of memory?

Sorry, the comment form is closed at this time.