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.