The 15th post of the Symfony2 components series is focused on the Filesystem component, which provides some basic utilities to work with the filesystem. It extends PHP built-in functions such as mkdir() or copy() to make them more portable and easier to use and test.

The Filesystem component provides basic utilities to deal with the filesystem

The Filesystem component provides basic utilities to deal with the filesystem

The problem

PHP built-in functions for filesystem operations mimic common utilities in Unix-like based systems to work with the filesystem. They borrow not only their names, but also the way they work. Because of this, their usage is not very intuitive, especially for Windows users. Take this code as an example:

mkdir(__DIR__ . '/dir/subdir');

First, you may not have permissions to create the directory “subdir” inside “dir”:

PHP Warning:  mkdir(): Permission denied in filesystem_test.php on line 5

Also, it may happen that you do have permissions, but the directory “dir” does not exist:

PHP Warning:  mkdir(): No such file or directory in filesystem_test.php on line 5

Both errors are pretty annoying, as they throw PHP warnings that cannot be intercepted. Also, there is no good reason (at least for me) to force us to create first “dir” and then “subdir”. By using the Filesystem component we can get rid off from all these annoying issues. They work like you would expect without reading the docs and take care of the small (or not so small) differences between Windows and Unix-like systems.

use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Exception\IOExceptionInterface;

$filesystem = new Filesystem();

try {
    $filesystem->mkdir(__DIR__ . '/dir/subdir');
} catch (IOExceptionInterface $e) {
    echo 'Error: ' . $e->getPath() . ' could not be created';
}

Yay! The mkdir() method of the component creates both directories recursively.

Another advantage is that we can get exceptions when something goes wrong, so if we try to create a directory in a place that we don’t have permissions, we will get an IOExceptionInterface:

$filesystem->mkdir('/root/dir/subdir');
Error: /root/dir/subdir could not be created

In general, methods accept strings, arrays or objects implementing the Traversable interface, so we can create several directories (or any other operation) in a single call. Remember that the Traversable interface is an abstract interface, IteratorAggregate or Iterator extend from it.

The following snippets are equivalent:

$filesystem->mkdir(__DIR__ . '/dir/subdir1');
$filesystem->mkdir(__DIR__ . '/dir/subdir2');
]);
$filesystem->mkdir([
    __DIR__ . '/dir/subdir1',
    __DIR__ . '/dir/subdir2'
]);
$dirCollection = new DirCollection([
    __DIR__ . '/dir/subdir1',
    __DIR__ . '/dir/subdir2'
]);
$filesystem->mkdir($dirCollection);

The DirCollection object is just a toy class implementing the Iterator interface:

class DirCollection implements Iterator
{

    protected $directories;

    protected $position;

    public function __construct(array $directories = [])
    {
        $this->directories = $directories;
        $this->position = 0;
    }

    public function current()
    {
        return $this->directories[$this->position];
    }

    public function next()
    {
        ++$this->position;
    }

    public function key()
    {
        return $this->position;
    }

    public function valid()
    {
        return isset($this->directories[$this->position]);
    }

    public function rewind()
    {
        $this->position = 0;
    }

}

We could also have used built-in iterators. For example, we could change the modification time of all files and directories belonging to a given directory recursively combining RecursiveDirectoryIterator and RecursiveIteratorIterator. With RecursiveDirectoryIterator we iterate recursively over filesystem directories and with RecursiveIteratorIterator we go over the whole hierarchy.

$filesystem = new Filesystem();

$directoryIterator = new \RecursiveDirectoryIterator(__DIR__ . '/origin', \FilesystemIterator::SKIP_DOTS);
$iterator = new \RecursiveIteratorIterator($directoryIterator, \RecursiveIteratorIterator::SELF_FIRST);

$filesystem->touch($iterator);

Useful methods

We have seen so far only two methods: mkdir() and touch(), but the component provides a few more. As they are well documented in the official docs, we’ll just list them here and try only a few of them:

  • copy($originFile, $targetFile, $override = false)
  • mkdir($dirs, $mode = 0777)
  • exists($files)
  • touch($files, $time = null, $atime = null)
  • remove($files)
  • chmod($files, $mode, $umask = 0000, $recursive = false)
  • chown($files, $user, $recursive = false)
  • chgrp($files, $group, $recursive = false)
  • rename($origin, $target, $overwrite = false)
  • symlink($originDir, $targetDir, $copyOnWindows = false)
  • makePathRelative($endPath, $startPath)
  • mirror($originDir, $targetDir, \Traversable $iterator = null, $options = array())
  • isAbsolutePath($file)
  • dumpFile($filename, $content, $mode = 0666)

The dumpFile() method is especially interesting, as dumps atomically content into a file. That means that we will never see a partially-written file, as it writes a temporary file first and then moves it to the new file location when it’s finished.

Finally, the makePathRelative() method is really useful when we want to have a relative path based on our current working directory.

$dir = '/home/raulfraile/filesystem/';

echo $dir; // /home/raulfraile/filesystem/
echo $filesystem->makePathRelative($dir, '/home/raulfraile'); // filesystem/

File locking

From 2.6, the component ships with another interesting feature: a simple file locking mechanism. You may want to implement a locking system for several reasons, but it’s common for avoiding overlapping in cron jobs.

For example, if we have a task that is executed every 5 minutes and those tasks take more than 5 minutes to finish, they will overlap. In a locking mechanism based on files, the first process creates the lock file, so other processes, when check that that file exists, will wait or finish. Once the first process finishes, it removes the lock file.

Its usage is very simple. The following code shows how to create a lock called “cron.lock”, so only one process is running at a time.

use Symfony\Component\Filesystem\LockHandler;

$lockHandler = new LockHandler('cron.lock');
if (!$lockHandler->lock()) {
    // another process is running, exit
    echo 'Another process is already running';

    return 1;
}

// do stuff...
sleep(100);

If we try to execute two processes, we get the error message in the second one:

$ php index.php &
[1] 10241

$ php index.php
Another process is already running

Who’s using it?

More info

Photo: Filesystem, by kleuske.