Monday, February 4, 2013

Making sense of vfsStream

vfsStream is a very cool PHP library that abstracts the file system. Its primary use case is in unit tests, as a way to mock file system activity, and it integrates nicely with PHPUnit, even being mentioned in the PHPUnit manual.

Sadly the documentation is a bit lacking, so this article will try provide some middle ground behind the unexplained usage example you'll find in a few places, and the dry API docs. (In fact skip the API docs completely - the source is more understandable.)

I'm going to show this completely outside PHPUnit, as that clouds the issues. Let's start with the include we need:
   require_once 'vfsStream/vfsStream.php';

(Aside: all tests here are being done with version 0.12.0, installed using pear.)

Here is our minimal example:

   vfsStream::setup('logs');
   file_put_contents(vfsStream::url('logs/test.log'),"Hello\n");


The first line creates our filesystem.
The second line creates a file called "test.log", and puts a string inside it.

There is something I want to emphasize here, as it confused me no end. We have not created a directory called "logs". We have created a virtual file system called "logs". We have no sub-directories at all. test.log lives in the root of the filesystem.

Here is our troubleshooting tool:
   require_once 'vfsStream/visitor/vfsStreamStructureVisitor.php';
   print_r(vfsStream::inspect(new vfsStreamStructureVisitor())->getStructure());

It outputs:
    Array
    (
        [logs] => Array
            (
                [test.log] => Hello
            )
    )


(Yes, I know it still looks like logs is a directory name there too. It isn't.)

Another way to troubleshoot this is using PHP functions directly:
    $dir=dir("vfs://logs");
    while(($entry=$dir->read())!==false)echo $entry."\n";


This outputs:
    test.log

Note: you cannot use "vfs://" as a url to get the root. This is like trying to access the root of a website with "http://". You need to use "http://example.com/" and you need to use "vfs://logs" for a virtual filesystem. As I said, I found this confusing, so I prefer to rewrite my above examples as follows (this also shows the complete code):

   
    require_once 'vfsStream/vfsStream.php';
    vfsStream::setup('_virtualroot_',null,array('logs'=>array()));
    file_put_contents(vfsStream::url('_virtualroot_/logs/test.log'),"Hello\n");
    print_r(vfsStream::inspect(new vfsStreamStructureVisitor())->getStructure());
    $dir=dir("vfs://_virtualroot_");
    while(($entry=$dir->read())!==false)echo $entry."\n";

    ?>

This time we do have a directory called "logs", which is in the root of a virtual file system called "_virtualroot_". You may hate that approach for its horrible verbosity. Me? I like it.

A few random other points about vfsStream:
  • chdir("vfs://logs") does not work. The Known Issues page lists a few more.
  • vfsStream::url("logs/test.log") simply returns "vfs://logs/test.log".
  • fopen($fname,"at") fails. The "t" is not supported. You have to change your code to use "a" or "ab" (aside: "ab" is better, as "a" could work differently on different systems).
  • Calling vfsStream::setup  a second time removes the previous filesystem, even if you use a different virtual filesystem name.
  • I've not worked out how to use the git version. It may be that my above examples do not work with the very latest version. If I work it out, and that is the case, I'll post additional examples.