Finder: Filesystem Search

Class Nette\Utils\Finder makes browsing the directory structure really easy.

All examples assume the following class alias is defined:

use Nette\Utils\Finder;

Searching for Files

How to find all *.txt files in $dir directory without recursing subdirectories?

foreach (Finder::findFiles('*.txt')->in($dir) as $key => $file) {
    echo $key; // $key is a string containing absolute filename with path
    echo $file; // $file is an instance of SplFileInfo
}

As a result, the finder returns instances of SplFileInfo.

If the directory does not exist, an UnexpectedValueException is thrown.

And what about searching for *.txt files in $dir including subdirectories? Instead of in(), use from():

foreach (Finder::findFiles('*.txt')->from($dir) as $file) {
    echo $file;
}

Search by more masks, even inside more directories within one iteration:

foreach (Finder::findFiles('*.txt', '*.php')
    ->in($dir1, $dir2) as $file) {
    ...
}

Parameters can also be arrays:

foreach (Finder::findFiles($masks)->in($dirs) as $file) {
    ...
}

Searching for *.txt files containing a number in the name:

foreach (Finder::findFiles('*[0-9]*.txt')->from($dir) as $file) {
    ...
}

Searching for *.txt files, except those containing ‘X’ in the name:

foreach (Finder::findFiles('*.txt')
    ->exclude('*X*')->from($dir) as $file) {
    ...
}

exclude() is specified just after findFiles(), thus it applies to filename.

Directories to omit can be specified using the exclude after from clause:

foreach (Finder::findFiles('*.php')
    ->from($dir)->exclude('temp', '.git') as $file) {
    ...
}

Here exclude() is after from(), thus it applies to the directory name.

And now something a bit more complicated: searching for *.txt files located in subdirectories starting with ‘te’, but not ‘temp’:

foreach (Finder::findFiles('te*/*.txt')
    ->exclude('temp*/*')->from($dir) as $file) {
    ...
}

Depth of search can be limited using the limitDepth() method.

Searching for Directories

In addition to files, it is possible to search for directories using Finder::findDirectories('subdir*'), or to search for files and directories: Finder::find('file.txt').

Filtering

You can also filter results. For example by size. This way we will traverse the files of size between 100B and 200B:

foreach (Finder::findFiles('*.php')->size('>=', 100)->size('<=', 200)
    ->from($dir) as $file) {
    ...
}

Or files changed in the last two weeks:

foreach (Finder::findFiles('*.php')->date('>', '- 2 weeks')
    ->from($dir) as $file) {
    ...
}

Here we traverse PHP files with number of lines greater than 1000. As a filter we use a custom callback:

$finder = Finder::findFiles('*.php')->filter(function ($file) {
    return count(file($file->getPathname())) > 1000;
})->from($dir);

You can go even further and extend the Nette\Utils\Finder class for example with a dimensions method using the extension methods:

Finder::extensionMethod('dimensions', function($finder, $width, $height){
    if (!preg_match('#^([=<>!]+)\s*(\d+)$#i', $width, $mW)
        || !preg_match('#^([=<>!]+)\s*(\d+)$#i', $height, $mH)
    ) {
        throw new InvalidArgumentException('Invalid dimensions predicate format.');
    }
    return $finder->filter(function ($file) use ($mW, $mH) {
        return $file->getSize() >= 12 && ($size = getimagesize($file->getPathname()))
            && (!$mW || Finder::compare($size[0], $mW[1], $mW[2]))
            && (!$mH || Finder::compare($size[1], $mH[1], $mH[2]));
    });
});

Finder, find images larger than 50px × 50px:

foreach (Finder::findFiles('*')
    ->dimensions('>50', '>50')->from($dir) as $file) {
    ...
}

Connection to Amazon S3

It's possible to use custom streams, for example Zend_Service_Amazon_S3:

$s3 = new Zend_Service_Amazon_S3($key, $secret);
$s3->registerStreamWrapper('s3');

foreach (Finder::findFiles('photos*')
    ->size('<=', 1e6)->in('s3://bucket-name') as $file) {
    echo $file;
}

Handy, right? You will certainly find a use for Finder in your applications.